diff --git a/.gitignore b/.gitignore index 603b140..aa724b7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /captures .externalNativeBuild .cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 43fd926..3c7772a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,12 +1,26 @@ + + - - + \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml deleted file mode 100644 index f2aa065..0000000 --- a/.idea/markdown-navigator.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml deleted file mode 100644 index db06266..0000000 --- a/.idea/markdown-navigator/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 89482aa..b45762e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,16 +1,11 @@ - - - - - - + - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index fdf1fc8..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,7 +1,6 @@ - - + \ No newline at end of file diff --git a/README.md b/README.md index c5a187c..eac3fba 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ # Reader -一款在任阅基础上改进的网络小说阅读器📕,采用Kt编写,支持追书、看书。拥有语音朗读,仿真翻页等功能。相比原作者: +一款网络小说阅读器📕,采用Kt编写,支持追书、看书。支持TTS语音朗读,仿真翻页等功能。相比old分支: - 重写代码逻辑,优化代码结构,Md设计风格 - - + 使用郭神的LitePal框架存储 - + + + sql分层处理,MVVM 架构 + * 语音朗读、插画、国际化、黑夜模式等功能都进行了实现 - - - 适配了安卓9.0,使用AndroidX控件,对刘海屏等异形屏幕有了很好的支持 - -注: 之前项目采用Java编写,之后才转的Kotlin代码,可能有些替换不完善。欢迎大家提问题。本人一定尽快修复 -## 使用技术 -Kt、AndroidX、Retrofit、Okhttp3、Glide、LitePal等 + - 适配了安卓10,使用AndroidX控件,对刘海屏等异形屏幕有了很好的支持 + +注: old分支之前项目采用Java编写,之后用studio一键转的Kotlin代码,没有kotlin特有的语法糖之类, +如果有同学想看java代码,old分支应该更容易懂。master分支为全新代码架构,欢迎大家提问题。本人一定尽快修复 + +## 使用技术 +Kt、AndroidX、Retrofit、Okhttp3、Glide、LitePal、MVVM、协程等 ## TIP -- 目前书源为轻小说书源,书中大多带了插图,因此屏蔽了tts语音朗读功能,之后会切换成正常小说书源并添加语音朗读功能 +- 当前书源为轻小说书源,书中大多带了插图。(不提供正常小说的原因有版权问题,同时防止有人直接用代码改个名字就去上架) + 因为书籍为爬取的数据,服务器在美国,所以国内监管比较严的时候可能需要翻墙或切换网络去多尝试几次。 -* 同时请各位大神手下留情不要攻击或者爬取数据,供大家学习使用。之后会切换成网络源。 +* 同时请大家不要攻击或者爬取数据,供大家学习使用。 ## 项目截图 @@ -28,9 +29,9 @@ Kt、AndroidX、Retrofit、Okhttp3、Glide、LitePal等 | --- | --- | --- | | 设置 | 搜索 | 阅读页 | | 菜单 | 目录 | 插页 | -| 插页2 | 夜间 | 编辑 | +| 插页2 | 夜间 | 编辑 | + - ## LICENSE @@ -50,3 +51,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` + diff --git a/app/.gitignore b/app/.gitignore index 796b96d..e25efbd 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,3 @@ /build +/release +/google \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0fccfd2..0d2dad3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,38 +1,162 @@ -apply plugin: 'com.android.application' - -apply plugin: 'kotlin-android' +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-android-extensions' +} -apply plugin: 'kotlin-android-extensions' +def name = "reader" +def code = 1 +def version = "1.0.0" android { - compileSdkVersion rootProject.ext.android.compileSdkVersion + compileSdkVersion 30 + buildToolsVersion "29.0.3" + defaultConfig { - applicationId rootProject.ext.android.applicationId - minSdkVersion rootProject.ext.android.minSdkVersion - targetSdkVersion rootProject.ext.android.targetSdkVersion - versionCode rootProject.ext.android.versionCode - versionName rootProject.ext.android.versionName + applicationId "com.novel.read" + minSdkVersion 21 + targetSdkVersion 30 + versionCode code + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters "armeabi", "arm64-v8a"// "armeabi-v7a", + } + multiDexEnabled true + } + + signingConfigs { + release { + keyAlias 'key0' + keyPassword '1qaz@WSX' + storeFile file('reader.jks') + storePassword '1qaz@WSX' + } } + buildTypes { + debug { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } release { - minifyEnabled false + buildConfigField "boolean", "LOG_DEBUG", "false" //不显示log + minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + + android.applicationVariants.all { variant -> + variant.outputs.all { + def flavor = variant.productFlavors[0].name + outputFileName = "${name}_${flavor}_${defaultConfig.versionName}.apk" + } } } + + flavorDimensions "mode" + productFlavors { + app { + dimension "mode" + manifestPlaceholders = [APP_CHANNEL_VALUE: "app"] + } + google { + dimension "mode" + applicationId "com.novel.read" + manifestPlaceholders = [APP_CHANNEL_VALUE: "google"] + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = '1.8' + } + } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation rootProject.ext.dependencies["junit"] - androidTestImplementation rootProject.ext.dependencies["runner"] - androidTestImplementation rootProject.ext.dependencies["espresso_core"] - implementation project(path: ':net_serivce') - implementation project(path: ':common_lib') + implementation fileTree(dir: 'libs', include: ['*.aar']) + testImplementation 'junit:junit:4.13.1' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation "com.android.support:multidex:1.0.3" + + //kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + //androidX + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.media:media:1.2.1' + implementation 'androidx.preference:preference-ktx:1.1.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.viewpager2:viewpager2:1.0.0' + implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android:flexbox:1.1.0' + implementation 'com.google.code.gson:gson:2.8.6' + + //基本的网络框架 + implementation 'com.squareup.okhttp3:okhttp:3.14.9' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.7.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' + + //anko + def anko_version = '0.10.8' + implementation "org.jetbrains.anko:anko-sdk27:$anko_version" + implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" + + //sql + implementation 'org.litepal.android:kotlin:3.0.0' + + //协程 + def coroutines_version = '1.3.7' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + + //liveEventBus + implementation 'com.jeremyliao:live-event-bus-x:1.7.2' + + //lifecycle + def lifecycle_version = '2.2.0' + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + + //Glide + implementation 'com.github.bumptech.glide:glide:4.11.0' + + //MarkDown + implementation 'io.noties.markwon:core:4.6.0' + implementation 'io.noties.markwon:image-glide:4.6.0' + //转换繁体 + implementation 'com.hankcs:hanlp:portable-1.7.8' + + //brvah + implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.6' + + //字符串比较 + implementation 'net.ricecode:string-similarity:1.0.0' + + //epub + implementation('com.positiondev.epublib:epublib-core:3.1') { + exclude group: 'org.slf4j' + exclude group: 'xmlpull' + } + + //flex + implementation 'com.google.android:flexbox:1.1.0' implementation 'com.allenliu.versionchecklib:library:2.2.0' -} + + implementation 'com.permissionx.guolindev:permissionx:1.4.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..bc660d7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,235 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +# 混合时不使用大小写混合,混合后的类名为小写 +-dontusemixedcaseclassnames + +# 指定不去忽略非公共库的类 +-dontskipnonpubliclibraryclasses + +# 这句话能够使我们的项目混淆后产生映射文件 +# 包含有类名->混淆后类名的映射关系 +-verbose + +# 指定不去忽略非公共库的类成员 +-dontskipnonpubliclibraryclassmembers + +# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。 +-dontpreverify + +# 保留Annotation不混淆 +-keepattributes *Annotation*,InnerClasses + +# 避免混淆泛型 +-keepattributes Signature + +# 抛出异常时保留代码行号 +-keepattributes SourceFile,LineNumberTable + +# 指定混淆是采用的算法,后面的参数是一个过滤器 +# 这个过滤器是谷歌推荐的算法,一般不做更改 +-optimizations !code/simplification/cast,!field/*,!class/merging/* + + + +############################################# +# +# Android开发中一些需要保留的公共部分 +# +############################################# +# 屏蔽错误Unresolved class name +#noinspection ShrinkerUnresolvedReference + +# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 +# 因为这些子类都有可能被外部调用 +# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 +# 因为这些子类都有可能被外部调用 +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class * extends android.view.View + +# 保留androidx下的所有类及其内部类 +-keep class androidx.** {*;} + +# 保留继承的 +-keep public class * extends androidx.** + +# 保留R下面的资源 +-keep class **.R$* {*;} + +# 保留本地native方法不被混淆 +-keepclasseswithmembernames class * { + native ; +} + +# 保留在Activity中的方法参数是view的方法, +# 这样以来我们在layout中写的onClick就不会被影响 +-keepclassmembers class * extends android.app.Activity{ + public void *(android.view.View); +} + +# 保留枚举类不被混淆 +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# 保留我们自定义控件(继承自View)不被混淆 +-keep public class * extends android.view.View{ + *** get*(); + void set*(***); + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); +} + +# 保留Parcelable序列化类不被混淆 +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +# 保留Serializable序列化的类不被混淆 +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆 +-keepclassmembers class * { + void *(**On*Event); + void *(**On*Listener); +} +# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用 +# 记得proguard-android.txt中一定不要加-dontoptimize才起作用 +# 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制 +-assumenosideeffects class android.util.Log { + public static int v(...); + public static int i(...); + public static int w(...); + public static int d(...); + public static int e(...); +} + +# 保持js引擎调用的java类 +-keep class **.analyzeRule.**{*;} +# 保持web类 +-keep class **.web.**{*;} +#数据类 +-keep class **.data.**{*;} + +-dontwarn rx.** +-dontwarn okio.** +-dontwarn javax.annotation.** +-dontwarn org.apache.log4j.lf5.viewer.** +-dontnote org.apache.log4j.lf5.viewer.** +-dontwarn freemarker.** +-dontnote org.python.core.** +-dontwarn com.hwangjr.rxbus.** +-dontwarn okhttp3.** +-dontwarn org.conscrypt.** +-dontwarn com.jeremyliao.liveeventbus.** + +-keep class com.google.gson.** { *; } +-keep class com.ke.gson.** { *; } +-keep class com.jeremyliao.liveeventbus.** { *; } +-keep class okhttp3.**{*;} +-keep class okio.**{*;} +-keep class com.hwangjr.rxbus.**{*;} +-keep class org.conscrypt.**{*;} +-keep class android.support.**{*;} +-keep class me.grantland.widget.**{*;} +-keep class de.hdodenhof.circleimageview.**{*;} +-keep class tyrant.explosionfield.**{*;} +-keep class tyrantgit.explosionfield.**{*;} +-keep class freemarker.**{*;} +-keep class com.gyf.barlibrary.** {*;} + +-keep class org.slf4j.**{*;} +-dontwarn org.slf4j.** + +-keep class org.codehaus.**{*;} +-dontwarn org.codehaus.** +-keep class com.jayway.**{*;} +-dontwarn com.jayway.** +-keep class com.fasterxml.**{*;} + +-keep class javax.swing.**{*;} +-dontwarn javax.swing.** +-keep class java.awt.**{*;} +-dontwarn java.awt.** +-keep class sun.misc.**{*;} +-dontwarn sun.misc.** +-keep class sun.reflect.**{*;} +-dontwarn sun.reflect.** + +## Rhino +-keep class javax.script.** { *; } +-keep class com.sun.script.javascript.** { *; } +-keep class org.mozilla.javascript.** { *; } + +###EPUB +-dontwarn nl.siegmann.epublib.** +-dontwarn org.xmlpull.** +-keep class nl.siegmann.epublib.**{*;} +-keep class javax.xml.**{*;} +-keep class org.xmlpull.**{*;} + +-keep class org.simpleframework.**{*;} +-dontwarn org.simpleframework.xml.** + +-keepclassmembers class * { + public (org.json.JSONObject); +} +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + + +-keep class com.burst.k17reader_sdk.** { *; } +-keep class com.chineseall.reader.utils.** { *; } + +-keep class org.litepal.** { *;} +-keep class * extends org.litepal.crud.DataSupport { *;} +-keep class * extends org.litepal.crud.LitePalSupport { *;} + +-keep class com.allenliu.versionchecklib.**{*;} + +-dontwarn com.tencent.bugly.** +-keep public class com.tencent.bugly.**{*;} + + +-keep class com.uc.** {*;} +-keep class com.zui.** {*;} +-keep class com.miui.** {*;} +-keep class com.heytap.** {*;} +-keep class a.** {*;} +-keep class com.vivo.** {*;} +-keep class com.umeng.** {*;} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-dontwarn com.google.ads.** +-keep public class com.google.ads.**{ + public protected *; +} + +-dontwarn com.youth.banner.** +-keep class com.youth.banner.**{*;} + +-keep class com.android.vending.billing.** diff --git a/app/reader.jks b/app/reader.jks new file mode 100644 index 0000000..9401a7f Binary files /dev/null and b/app/reader.jks differ diff --git a/app/src/androidTest/java/com/novel/read/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/novel/read/ExampleInstrumentedTest.kt index a2dbbae..a15eac6 100644 --- a/app/src/androidTest/java/com/novel/read/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/novel/read/ExampleInstrumentedTest.kt @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.zlj.base_libaray", appContext.packageName) + assertEquals("com.yjd.tuzibook", appContext.packageName) } -} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d784d9c..09b0643 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,71 +3,96 @@ xmlns:tools="http://schemas.android.com/tools" package="com.novel.read"> + + + - - - - + + + + + + + - - - - + android:requestLegacyExternalStorage="true" + android:supportsRtl="true" + android:theme="@style/AppTheme.Light" + tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute"> + + + + + + + + + + + android:name="com.novel.read.ui.daily.DailyActivity" + android:launchMode="singleTask" /> + android:name="com.novel.read.ui.channel.ChannelInfoActivity" + android:launchMode="singleTask" /> + android:name="com.novel.read.ui.channel.ChannelActivity" + android:launchMode="singleTask" + android:screenOrientation="behind" /> + + android:name="com.novel.read.ui.search.SearchActivity" + android:launchMode="singleTask" /> + android:name="com.novel.read.ui.read.ReadBookActivity" + android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" + android:launchMode="singleTask" /> + + + - - - + + + - + \ No newline at end of file diff --git a/app/src/main/assets/bg/羊皮纸1.jpg b/app/src/main/assets/bg/羊皮纸1.jpg new file mode 100644 index 0000000..40f3977 Binary files /dev/null and b/app/src/main/assets/bg/羊皮纸1.jpg differ diff --git a/app/src/main/assets/defaultData/httpTTS.json b/app/src/main/assets/defaultData/httpTTS.json new file mode 100644 index 0000000..a249a3b --- /dev/null +++ b/app/src/main/assets/defaultData/httpTTS.json @@ -0,0 +1,62 @@ +[ + { + "id": 1598233029304, + "name": "度小美", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029305, + "name": "度小宇", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029306, + "name": "度逍遥", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029307, + "name": "度丫丫", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029308, + "name": "度小娇", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029309, + "name": "度米朵", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029310, + "name": "度博文", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029311, + "name": "度小童", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029312, + "name": "度小萌", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029313, + "name": "百度骚男", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=11&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029314, + "name": "百度评书", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": 1598233029315, + "name": "百度主持", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/readConfig.json b/app/src/main/assets/defaultData/readConfig.json new file mode 100644 index 0000000..32c8ea4 --- /dev/null +++ b/app/src/main/assets/defaultData/readConfig.json @@ -0,0 +1,52 @@ +[ + { + "bgStr": "#EFEFF7", + "bgStrNight": "#212021", + "textColor": "#383429", + "textColorNight": "#94969C", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconNight": false + }, + { + "bgStr": "#DDC090", + "bgStrNight": "#3C3F43", + "textColor": "#3E3422", + "textColorNight": "#DCDFE1", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconNight": false + }, + { + "bgStr": "#C2D8AA", + "bgStrNight": "#3C3F43", + "textColor": "#596C44", + "textColorNight": "#88C16F", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + }, + { + "bgStr": "#DBB8E2", + "bgStrNight": "#3C3F43", + "textColor": "#68516C", + "textColorNight": "#F6AEAE", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + }, + { + "bgStr": "#ABCEE0", + "bgStrNight": "#3C3F43", + "textColor": "#3D4C54", + "textColorNight": "#90BFF5", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/themeConfig.json b/app/src/main/assets/defaultData/themeConfig.json new file mode 100644 index 0000000..a8826cf --- /dev/null +++ b/app/src/main/assets/defaultData/themeConfig.json @@ -0,0 +1,26 @@ +[ + { + "themeName": "典雅蓝", + "isNightTheme": false, + "primaryColor": "#03A9F4", + "accentColor": "#AD1457", + "backgroundColor": "#F5F5F5", + "bottomBackground": "#EEEEEE" + }, + { + "themeName": "黑白", + "isNightTheme": true, + "primaryColor": "#303030", + "accentColor": "#E0E0E0", + "backgroundColor": "#424242", + "bottomBackground": "#424242" + }, + { + "themeName": "A屏黑", + "isNightTheme": true, + "primaryColor": "#000000", + "accentColor": "#FFFFFF", + "backgroundColor": "#000000", + "bottomBackground": "#000000" + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/txtTocRule.json b/app/src/main/assets/defaultData/txtTocRule.json new file mode 100644 index 0000000..57cb4bf --- /dev/null +++ b/app/src/main/assets/defaultData/txtTocRule.json @@ -0,0 +1,128 @@ +[ + { + "id": -1, + "enable": true, + "name": "目录(去空白)", + "rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", + "serialNumber": 0 + }, + { + "id": -2, + "enable": true, + "name": "目录", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", + "serialNumber": 1 + }, + { + "id": -3, + "enable": false, + "name": "目录(匹配简介)", + "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", + "serialNumber": 2 + }, + { + "id": -4, + "enable": false, + "name": "目录(古典、轻小说备用)", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$", + "serialNumber": 3 + }, + { + "id": -5, + "enable": false, + "name": "数字(纯数字标题)", + "rule": "(?<=[ \\s])\\d+\\.?[  \\t]{0,4}$", + "serialNumber": 4 + }, + { + "id": -6, + "enable": true, + "name": "数字 分隔符 标题名称", + "rule": "^[  \\t]{0,4}\\d{1,5}[::,., 、_—\\-].{1,30}$", + "serialNumber": 5 + }, + { + "id": -7, + "enable": true, + "name": "大写数字 分隔符 标题名称", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 、_—\\-].{1,30}$", + "serialNumber": 6 + }, + { + "id": -8, + "enable": true, + "name": "正文 标题/序号", + "rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$", + "serialNumber": 7 + }, + { + "id": -9, + "enable": true, + "name": "Chapter/Section/Part/Episode 序号 标题", + "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][oO]\\.|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$", + "serialNumber": 8 + }, + { + "id": -10, + "enable": false, + "name": "Chapter(去简介)", + "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][Oo]\\.|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$", + "serialNumber": 9 + }, + { + "id": -11, + "enable": true, + "name": "特殊符号 序号 标题", + "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", + "serialNumber": 10 + }, + { + "id": -12, + "enable": false, + "name": "特殊符号 标题(成对)", + "rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", + "serialNumber": 11 + }, + { + "id": -13, + "enable":true, + "name": "特殊符号 标题(单个)", + "rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", + "serialNumber": 12 + }, + { + "id": -14, + "enable": true, + "name": "章/卷 序号 标题", + "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", + "serialNumber": 13 + }, + { + "id": -15, + "enable":false, + "name": "顶格标题", + "rule": "^\\S.{1,20}$", + "serialNumber": 14 + }, + { + "id": -16, + "enable":false, + "name": "双标题(前向)", + "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", + "serialNumber": 15 + }, + { + "id": -17, + "enable":false, + "name": "双标题(后向)", + "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", + "serialNumber": 16 + }, + { + "id":-18, + "enable": true, + "name": "标题 特殊符号 序号", + "rule": "^.{1,20}[((][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$", + "serialNumber": 17 + } +] \ No newline at end of file diff --git a/app/src/main/assets/font/number.ttf b/app/src/main/assets/font/number.ttf new file mode 100644 index 0000000..f2804db Binary files /dev/null and b/app/src/main/assets/font/number.ttf differ diff --git a/app/src/main/assets/litepal.xml b/app/src/main/assets/litepal.xml index 486ab39..6c0b221 100644 --- a/app/src/main/assets/litepal.xml +++ b/app/src/main/assets/litepal.xml @@ -1,23 +1,13 @@ - - - - - + + - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/App.kt b/app/src/main/java/com/novel/read/App.kt new file mode 100644 index 0000000..4a8262a --- /dev/null +++ b/app/src/main/java/com/novel/read/App.kt @@ -0,0 +1,123 @@ +package com.novel.read + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate +import androidx.multidex.MultiDexApplication +import com.jeremyliao.liveeventbus.LiveEventBus +import com.novel.read.constant.AppConst.channelIdDownload +import com.novel.read.constant.AppConst.channelIdReadAloud +import com.novel.read.constant.EventBus +import com.novel.read.data.db.BookDatabase +import com.novel.read.help.ActivityHelp +import com.novel.read.help.AppConfig +import com.novel.read.help.ReadBookConfig +import com.novel.read.help.ThemeConfig +import com.novel.read.utils.LanguageUtils +import com.novel.read.utils.ext.postEvent +import org.litepal.LitePal + +@Suppress("DEPRECATION") +class App : MultiDexApplication() { + + companion object { + @JvmStatic + lateinit var INSTANCE: App + private set + + @JvmStatic + lateinit var db: BookDatabase + private set + + lateinit var androidId: String + var versionCode = 0 + var versionName = "" + } + + override fun onCreate() { + super.onCreate() + INSTANCE = this + androidId = Settings.System.getString(contentResolver, Settings.Secure.ANDROID_ID) + LanguageUtils.setConfiguration(this) + LitePal.initialize(this) + db = BookDatabase.get() + packageManager.getPackageInfo(packageName, 0)?.let { + versionCode = it.versionCode + versionName = it.versionName + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId() + applyDayNight() + LiveEventBus + .config() + .lifecycleObserverAlwaysActive(true) + .autoClear(false) + registerActivityLifecycleCallbacks(ActivityHelp) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES, + Configuration.UI_MODE_NIGHT_NO -> applyDayNight() + } + } + + + fun applyDayNight() { + AppConfig.upEInkMode() + ReadBookConfig.upBg() + ThemeConfig.applyTheme(this) + initNightMode() + postEvent(EventBus.RECREATE, "") + } + + + private fun initNightMode() { + val targetMode = + if (AppConfig.isNightTheme) { + AppCompatDelegate.MODE_NIGHT_YES + } else { + AppCompatDelegate.MODE_NIGHT_NO + } + AppCompatDelegate.setDefaultNightMode(targetMode) + } + + /** + * 创建通知ID + */ + @RequiresApi(Build.VERSION_CODES.O) + private fun createChannelId() { + (getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let { + //用唯一的ID创建渠道对象 + val downloadChannel = NotificationChannel( + channelIdDownload, + getString(R.string.action_download), + NotificationManager.IMPORTANCE_LOW + ) + //初始化channel + downloadChannel.enableLights(false) + downloadChannel.enableVibration(false) + downloadChannel.setSound(null, null) + + //用唯一的ID创建渠道对象 + val readAloudChannel = NotificationChannel( + channelIdReadAloud, + getString(R.string.read_aloud), + NotificationManager.IMPORTANCE_LOW + ) + //初始化channel + readAloudChannel.enableLights(false) + readAloudChannel.enableVibration(false) + readAloudChannel.setSound(null, null) + + //向notification manager 提交channel + it.createNotificationChannels(listOf(downloadChannel, readAloudChannel)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/Ext.kt b/app/src/main/java/com/novel/read/Ext.kt deleted file mode 100644 index 72e8070..0000000 --- a/app/src/main/java/com/novel/read/Ext.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.novel.read - -import android.content.Context -import android.widget.ImageView -import android.widget.Toast - -fun Context.showToast(msg:String){ - Toast.makeText(this,msg,Toast.LENGTH_SHORT).show() -} - -fun Context.getScreenContentWidth(): Int { - val displayMetrics = this.resources.displayMetrics - return displayMetrics.widthPixels -} - -fun Context.dp2px(dps: Int): Int { - return Math.round(dps.toFloat() * getDensityDpiScale(this)) -} - -fun getDensityDpiScale(context: Context): Float { - return context.resources.displayMetrics.xdpi / 160.0f -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/activity/NovelBookDetailActivity.kt b/app/src/main/java/com/novel/read/activity/NovelBookDetailActivity.kt deleted file mode 100644 index 86f1945..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelBookDetailActivity.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.novel.read.activity - -import android.annotation.SuppressLint -import android.app.ProgressDialog -import android.content.Intent -import android.util.Log -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.adapter.LoveLyAdapter -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.RequestCode.Companion.REQUEST_READ -import com.novel.read.constants.Constant.ResultCode.Companion.RESULT_IS_COLLECTED -import com.novel.read.event.BookArticleEvent -import com.novel.read.event.GetBookDetailEvent -import com.novel.read.event.GetRecommendBookEvent -import com.novel.read.event.UpdateBookEvent -import com.novel.read.http.AccountManager -import com.novel.read.model.db.CollBookBean -import com.novel.read.model.db.dbManage.BookRepository -import com.novel.read.model.protocol.RecommendBookResp -import com.novel.read.showToast -import com.novel.read.utlis.DateUtli -import com.novel.read.utlis.GlideImageLoader -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.activity_book_detail.* -import org.litepal.LitePal -import java.util.* - -class NovelBookDetailActivity(override val layoutId: Int = R.layout.activity_book_detail) : NovelBaseActivity(), View.OnClickListener { - - private lateinit var mAdapter: LoveLyAdapter - private val mList = ArrayList() - private var mBookId: Int = 0 - private var isCollected = false - private var mCollBookBean: CollBookBean? = null - private lateinit var mProgressDialog: ProgressDialog - - override fun initView() { - mBookId = intent.getIntExtra(Constant.Bundle.BookId, 0) - rlv_lovely.layoutManager = LinearLayoutManager(this) - mAdapter = LoveLyAdapter(mList) - rlv_lovely.adapter = mAdapter - mProgressDialog = ProgressDialog(this) - } - - override fun initData() { - refresh.showLoading() - refresh.setOnReloadingListener { getData() } - getData() - toolbar.setNavigationOnClickListener { finish() } - tv_add_book.setOnClickListener(this) - tv_start_read.setOnClickListener(this) - } - - private fun getData() { - AccountManager.getInstance().getRecommendBook(mBookId.toString(), "10") - AccountManager.getInstance().getBookDetail(mBookId.toString()) - } - - override fun onClick(v: View?) { - when (v?.id) { - R.id.tv_add_book -> - //点击存储 - if (isCollected) { - //放弃点击 - BookRepository.getInstance().deleteCollBookInRx(mCollBookBean) - tv_add_book.text = resources.getString(R.string.add_book) - isCollected = false - } else { - mProgressDialog.setTitle("正在添加到书架中") - mProgressDialog.show() - AccountManager.getInstance().getBookArticle(mBookId.toString()) - - } - R.id.tv_start_read -> - startActivityForResult( - Intent(this, NovelReadActivity::class.java) - .putExtra(NovelReadActivity.EXTRA_IS_COLLECTED, isCollected) - .putExtra(NovelReadActivity.EXTRA_COLL_BOOK, mCollBookBean), REQUEST_READ - ) - } - } - - @SuppressLint("SetTextI18n") - @Subscribe - fun getBookDetail(event: GetBookDetailEvent) { - refresh.showFinish() - if (event.isFail) { - refresh.showError() - } else { - val bookBean = event.result!!.book - GlideImageLoader.displayCornerImage(this, bookBean.cover, iv_book) - tv_book_name.text = bookBean.title - - tv_book_author.text = bookBean.author + " | " - tv_book_length.text = getString(R.string.book_word, bookBean.words / 10000) - - if (event.result!!.last_article != null) { - tv_new_title.text = - getString(R.string.new_chapter, event.result!!.last_article.title) - tv_update_time.text = - DateUtli.dateConvert(event.result!!.last_article.create_time, 0) - } - - tv_human_num.text = bookBean.hot.toString() + "" - tv_love_look_num.text = bookBean.like - tv_Intro.text = bookBean.description - mCollBookBean = BookRepository.getInstance().getCollBook(bookBean.id.toString()) - //判断是否收藏 - if (mCollBookBean != null) { - isCollected = true - tv_add_book.text = resources.getString(R.string.already_add) - tv_start_read.text = resources.getString(R.string.go_read) - } else { - mCollBookBean = event.result!!.collBookBean - } - } - } - - @Subscribe - fun getRecommendBook(event: GetRecommendBookEvent) { - if (event.isFail) { - Log.e("getRecommendBook", event.er?.msg) - } else { - mList.clear() - mList.addAll(event.result!!.book) - mAdapter.notifyDataSetChanged() - } - } - - override fun onResume() { - super.onResume() - EventManager.instance.registerSubscriber(this) - } - - override fun onPause() { - super.onPause() - EventManager.instance.unregisterSubscriber(this) - } - - @Subscribe - fun getArticle(event: BookArticleEvent) { - if (event.isFail) { - dismiss() - showToast(getString(R.string.net_error)) - } else { - //存储收藏 - var success = false - if (mCollBookBean != null) { - success = mCollBookBean!!.saveOrUpdate("bookId=?", mCollBookBean!!.id) - } - if (success) { - val bookChapterBean = event.result!!.chapterBean - for (i in bookChapterBean.indices) { - bookChapterBean[i].collBookBean = mCollBookBean - } - LitePal.saveAllAsync(bookChapterBean).listen { success1 -> - if (success1) { - if (tv_add_book != null) { - tv_add_book.text = resources.getString(R.string.already_add) - } - isCollected = true - } else { - LitePal.deleteAll(CollBookBean::class.java, "bookId =?", mCollBookBean!!.id) - showToast(getString(R.string.net_error)) - } - dismiss() - } - } else { - showToast(getString(R.string.net_error)) - dismiss() - } - } - - } - - private fun dismiss() { - mProgressDialog.dismiss() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - //如果进入阅读页面收藏了,页面结束的时候,就需要返回改变收藏按钮 - if (requestCode == REQUEST_READ) { - if (data == null) { - return - } - - isCollected = data.getBooleanExtra(RESULT_IS_COLLECTED, false) - - if (isCollected) { - tv_add_book.text = resources.getString(R.string.already_add) - tv_start_read.text = resources.getString(R.string.go_read) - } - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.postEvent(UpdateBookEvent()) - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelBookTypeListActivity.kt b/app/src/main/java/com/novel/read/activity/NovelBookTypeListActivity.kt deleted file mode 100644 index 4c755d6..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelBookTypeListActivity.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.novel.read.activity - -import androidx.recyclerview.widget.LinearLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.adapter.BookListAdapter -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.COMMENT_SIZE -import com.novel.read.event.SearchListEvent -import com.novel.read.http.AccountManager -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.protocol.SearchResp -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.activity_book_type_list.* -import java.util.* - -class NovelBookTypeListActivity(override val layoutId: Int = R.layout.activity_book_type_list) : NovelBaseActivity() { - - private var mList: MutableList = ArrayList() - private lateinit var mAdapter: BookListAdapter - private var mCategoryId: String? = null - private var mTitle: String? = null - private var page: Int = 1 - private var loadSize: Int = 0 - - override fun initView() { - EventManager.instance.registerSubscriber(this) - - mCategoryId = intent.getStringExtra(Constant.Bundle.CategoryId) - mTitle = intent.getStringExtra(Constant.Bundle.mTitle) - - rlv_type_list.layoutManager = LinearLayoutManager(this) - mAdapter = BookListAdapter(mList, rlv_type_list) - rlv_type_list.adapter = mAdapter - - mAdapter.setOnLoadMoreListener(object : OnLoadMoreListener { - override fun onLoadMore() { - if (mAdapter.isLoadingMore) { - rlv_type_list.stopScroll() - } else { - if (loadSize >= COMMENT_SIZE) { - mAdapter.isLoadingMore = true - mAdapter.notifyDataSetChanged() - page++ - getData() - } - } - } - }) - } - - override fun initData() { - refresh.showLoading() - refresh.setOnReloadingListener { this.getData() } - getData() - toolbar.title = mTitle - toolbar.setNavigationOnClickListener { finish() } - - } - - private fun getData() { - mCategoryId?.let { AccountManager.getInstance().getSearchBookList(it, "", page) } - } - - @Subscribe - fun getSearchList(event: SearchListEvent) { - refresh.showFinish() - if (event.isFail) { - refresh.showError() - } else { - loadSize = event.result!!.book.size - if (mAdapter.isLoadingMore) { - mAdapter.isLoadingMore = false - mList.addAll(event.result!!.book) - mAdapter.notifyDataSetChanged() - } else { - mList.clear() - mList.addAll(event.result!!.book) - mAdapter.notifyDataSetChanged() - } - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelMainActivity.kt b/app/src/main/java/com/novel/read/activity/NovelMainActivity.kt deleted file mode 100644 index 3d2d5e3..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelMainActivity.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.novel.read.activity - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.view.KeyEvent -import android.view.View -import androidx.fragment.app.Fragment -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.event.* -import com.novel.read.fragment.BookFragment -import com.novel.read.fragment.MoreFragment -import com.novel.read.fragment.RecommendFragment -import com.novel.read.fragment.StackFragment -import com.novel.read.http.AccountManager -import com.novel.read.model.db.dbManage.BookRepository -import com.novel.read.showToast -import com.novel.read.utlis.SpUtil -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.activity_main.* - -class NovelMainActivity(override val layoutId: Int = R.layout.activity_main) : NovelBaseActivity() { - - private lateinit var mCurrentFrag: Fragment - private lateinit var mMainFragment: BookFragment - private lateinit var mRecommendFragment: RecommendFragment - private lateinit var mStackFragment: StackFragment - private lateinit var mMoreFragment: MoreFragment - - //记录用户首次点击返回键的时间 - private var firstTime: Long = 0 - - override fun initView() { - mCurrentFrag = Fragment() - mMainFragment = BookFragment.newInstance() - mRecommendFragment = RecommendFragment.newInstance() - mStackFragment = StackFragment.newInstance() - mMoreFragment = MoreFragment.newInstance() - AccountManager.getInstance().login(this) - } - - override fun initData() { - bottom_bar.setOnNavigationItemSelectedListener { menuItem -> - when (menuItem.itemId) { - R.id.tab_one -> { - switchFragment(mMainFragment) - return@setOnNavigationItemSelectedListener true - } - R.id.tab_two -> { - switchFragment(mRecommendFragment) - return@setOnNavigationItemSelectedListener true - } - R.id.tab_three -> { - switchFragment(mStackFragment) - return@setOnNavigationItemSelectedListener true - } - R.id.tab_four -> { - switchFragment(mMoreFragment) - return@setOnNavigationItemSelectedListener true - } - } - false - } - - if (BookRepository.getInstance().collBooks.size > 0) { - switchFragment(mMainFragment) - } else { - bottom_bar.selectedItemId = R.id.tab_two - } - } - - private fun switchFragment(targetFragment: Fragment) { - val transaction = supportFragmentManager.beginTransaction() - if (!targetFragment.isAdded) { - transaction.hide(mCurrentFrag) - transaction.add(R.id.fl_content, targetFragment, targetFragment.javaClass.name) - } else { - transaction.hide(mCurrentFrag).show(targetFragment) - } - mCurrentFrag = targetFragment - transaction.commit() - } - - override fun onResume() { - super.onResume() - EventManager.instance.registerSubscriber(this) - } - - override fun onPause() { - super.onPause() - EventManager.instance.unregisterSubscriber(this) - } - - @Subscribe - fun login(event: LoginEvent) { - if (event.isFail) { - Log.e("NovelMainActivity", "login: " + event.er!!.msg) - } else { - SpUtil.setStringValue(Constant.Uid, event.result!!.uid.toString()) - } - } - - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { - if (!isVisible(bottom_bar)) { - bottom_bar.visibility = View.VISIBLE - mMainFragment.updateBook(UpdateBookEvent()) - } else { - val secondTime = System.currentTimeMillis() - if (secondTime - firstTime > 1000) { - firstTime = secondTime - showToast("再次点击退出界面") - } else { - finish() - } - } - return true - } - return super.onKeyUp(keyCode, event) - } - - @Subscribe - fun setBottomBar(event: HideBottomBarEvent) { - if (event.result!!) { - bottom_bar.visibility = View.GONE - } else { - bottom_bar.visibility = View.VISIBLE - } - } - - @Subscribe - fun toRecommendFragment(event: SwitchFragmentEvent) { - bottom_bar.selectedItemId = R.id.tab_two - } - - companion object { - fun reStart(context: Context) { - val intent = Intent(context, NovelMainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - } - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelRankListActivity.kt b/app/src/main/java/com/novel/read/activity/NovelRankListActivity.kt deleted file mode 100644 index e6b5d1b..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelRankListActivity.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.novel.read.activity - -import androidx.recyclerview.widget.LinearLayoutManager -import com.novel.read.R -import com.novel.read.adapter.RankListAdapter -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.COMMENT_SIZE -import com.novel.read.http.AccountManager -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.protocol.RankByUpdateResp -import kotlinx.android.synthetic.main.activity_rank_list.* -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.util.* - -/** - * 推荐fragment中点击更多跳转来的。 - */ -class NovelRankListActivity(override val layoutId: Int= R.layout.activity_rank_list) : NovelBaseActivity() { - - private lateinit var mAdapter: RankListAdapter - private var mList: MutableList = ArrayList() - private var page = 1 - private var loadSize: Int = 0 - private var type: String = "" - private var sex: String = "" - - override fun initView() { - rlv_book_list.layoutManager = LinearLayoutManager(this) - mAdapter = RankListAdapter(mList, rlv_book_list) - rlv_book_list.adapter = mAdapter - sex = intent.getStringExtra(Constant.Sex) - type = intent.getStringExtra(Constant.Type) - when (type) { - Constant.ListType.Human -> toolbar.title = getString(R.string.popular_selection) - Constant.ListType.EditRecommend -> toolbar.title = getString(R.string.edit_recommend) - Constant.ListType.HotSearch -> toolbar.title = getString(R.string.hot_search) - } - getData() - } - - override fun initData() { - toolbar.setNavigationOnClickListener { finish() } - - mAdapter.setOnLoadMoreListener(object : OnLoadMoreListener { - override fun onLoadMore() { - if (mAdapter.isLoadingMore) { - - } else { - if (loadSize >= COMMENT_SIZE) { - mAdapter.isLoadingMore = true - mAdapter.notifyDataSetChanged() - page++ - getData() - } - } - } - }) - } - - private fun getData() { - AccountManager.getInstance().getRankList(type, sex, Constant.DateTyp.Week, page.toString(), RankCallBack()) - } - - private inner class RankCallBack : Callback { - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - if (response.body() != null) { - loadSize = response.body()!!.book.size - if (mAdapter.isLoadingMore) { - mAdapter.isLoadingMore = false - mList.addAll(response.body()!!.book) - mAdapter.notifyDataSetChanged() - } else { - mList.clear() - mList.addAll(response.body()!!.book) - mAdapter.notifyDataSetChanged() - } - - } - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelReadActivity.kt b/app/src/main/java/com/novel/read/activity/NovelReadActivity.kt deleted file mode 100644 index 1d754e8..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelReadActivity.kt +++ /dev/null @@ -1,655 +0,0 @@ -package com.novel.read.activity - -import android.annotation.SuppressLint -import android.app.Activity -import android.app.Service -import android.content.* -import android.os.Build -import android.os.Handler -import android.os.IBinder -import android.os.Message -import android.util.Log -import android.view.KeyEvent -import android.view.View.* -import android.view.WindowManager -import android.view.animation.Animation -import android.view.animation.AnimationUtils -import android.widget.SeekBar -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.view.GravityCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.recyclerview.widget.LinearLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.adapter.CategoryAdapter -import com.novel.read.adapter.MarkAdapter -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.ResultCode.Companion.RESULT_IS_COLLECTED -import com.novel.read.event.BookArticleEvent -import com.novel.read.event.ErrorChapterEvent -import com.novel.read.event.FinishChapterEvent -import com.novel.read.event.RxBus -import com.novel.read.http.AccountManager -import com.novel.read.model.db.BookChapterBean -import com.novel.read.model.db.BookSignTable -import com.novel.read.model.db.CollBookBean -import com.novel.read.model.db.DownloadTaskBean -import com.novel.read.model.db.dbManage.BookRepository -import com.novel.read.service.DownloadMessage -import com.novel.read.service.DownloadService -import com.novel.read.showToast -import com.novel.read.utlis.BrightnessUtils -import com.novel.read.utlis.ScreenUtils -import com.novel.read.utlis.SpUtil -import com.novel.read.utlis.SystemBarUtils -import com.novel.read.widget.dialog.ReadSettingDialog -import com.novel.read.widget.page.PageLoader -import com.novel.read.widget.page.PageView -import com.novel.read.widget.page.ReadSettingManager -import com.novel.read.widget.page.TxtChapter -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.activity_read.* -import kotlinx.android.synthetic.main.layout_download.* -import kotlinx.android.synthetic.main.layout_light.* -import kotlinx.android.synthetic.main.layout_read_mark.* -import java.util.* - -/** - * 阅读页📕 - */ -class NovelReadActivity : NovelBaseActivity(), DownloadService.OnDownloadListener { - - private lateinit var mCategoryAdapter: CategoryAdapter - private val mChapters = ArrayList() - private lateinit var mCurrentChapter: TxtChapter //当前章节 - private var currentChapter = 0 - private lateinit var mMarkAdapter: MarkAdapter - private val mMarks = ArrayList() - private lateinit var mPageLoader: PageLoader - private var mTopInAnim: Animation? = null - private var mTopOutAnim: Animation? = null - private var mBottomInAnim: Animation? = null - private var mBottomOutAnim: Animation? = null - - private lateinit var mSettingDialog: ReadSettingDialog - private var isCollected = false // isFromSDCard - private var isNightMode = false - private var isFullScreen = false - private val isRegistered = false - - private lateinit var mCollBook: CollBookBean - private lateinit var mBookId: String - - @SuppressLint("HandlerLeak") - private val mHandler = object : Handler() { - override fun handleMessage(msg: Message) { - super.handleMessage(msg) - when (msg.what) { - WHAT_CATEGORY -> rlv_list.setSelection(mPageLoader.chapterPos) - WHAT_CHAPTER -> mPageLoader.openChapter() - } - } - } - - override val layoutId: Int get() = R.layout.activity_read - - // 接收电池信息和时间更新的广播 - private val mReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (Objects.requireNonNull(intent.action) == Intent.ACTION_BATTERY_CHANGED) { - val level = intent.getIntExtra("level", 0) - mPageLoader.updateBattery(level) - } else if (intent.action == Intent.ACTION_TIME_TICK) { - mPageLoader.updateTime() - }// 监听分钟的变化 - } - } - - private var mService: DownloadService.IDownloadManager? = null - private var mConn: ServiceConnection? = null - - override fun initView() { - EventManager.instance.registerSubscriber(this) - mCollBook = intent.getSerializableExtra(EXTRA_COLL_BOOK) as CollBookBean - isCollected = intent.getBooleanExtra(EXTRA_IS_COLLECTED, false) - mBookId = mCollBook.id - initService() - // 如果 API < 18 取消硬件加速 - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { - read_pv_page.setLayerType(LAYER_TYPE_SOFTWARE, null) - } - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - - //获取页面加载器 - mPageLoader = read_pv_page.getPageLoader(mCollBook) - - mSettingDialog = ReadSettingDialog(this, mPageLoader) - //禁止滑动展示DrawerLayout - read_dl_slide.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - //侧边打开后,返回键能够起作用 - read_dl_slide.isFocusableInTouchMode = false - //半透明化StatusBar - SystemBarUtils.transparentStatusBar(this) - //隐藏StatusBar - read_pv_page.post { this.hideSystemBar() } - read_abl_top_menu.setPadding(0, ScreenUtils.statusBarHeight, 0, 0) - ll_download.setPadding(0, ScreenUtils.statusBarHeight, 0, ScreenUtils.dpToPx(15)) - - val lp = window.attributes - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - lp.layoutInDisplayCutoutMode = 1 - } - window.attributes = lp - - //设置当前Activity的Brightness - if (ReadSettingManager.getInstance().isBrightnessAuto) { - BrightnessUtils.setDefaultBrightness(this) - } else { - BrightnessUtils.setBrightness(this, ReadSettingManager.getInstance().brightness) - } - - //注册广播 - val intentFilter = IntentFilter() - intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED) - intentFilter.addAction(Intent.ACTION_TIME_TICK) - registerReceiver(mReceiver, intentFilter) - - if (!SpUtil.getBooleanValue(Constant.BookGuide, false)) { - iv_guide.visibility = VISIBLE - toggleMenu(false) - } - - if (isCollected) { - mPageLoader.collBook.bookChapters = BookRepository.getInstance().getBookChaptersInRx(mBookId) - // 刷新章节列表 - mPageLoader.refreshChapterList() - // 如果是网络小说并被标记更新的,则从网络下载目录 - if (mCollBook.isUpdate && !mCollBook.isLocal) { - AccountManager.getInstance().getBookArticle(mBookId) - } - } else { - AccountManager.getInstance().getBookArticle(mBookId) - } - - } - - override fun initData() { - tv_book_name.text = mCollBook.title - mCategoryAdapter = CategoryAdapter() - rlv_list.adapter = mCategoryAdapter - rlv_list.isFastScrollEnabled = true - rlv_mark.layoutManager = LinearLayoutManager(this) - mMarkAdapter = MarkAdapter(mMarks) - rlv_mark.adapter = mMarkAdapter - isNightMode = ReadSettingManager.getInstance().isNightMode - //夜间模式按钮的状态 - toggleNightMode() - isFullScreen = ReadSettingManager.getInstance().isFullScreen - toolbar.setNavigationOnClickListener { finish() } - read_setting_sb_brightness.progress = ReadSettingManager.getInstance().brightness - mPageLoader.setOnPageChangeListener( - object : PageLoader.OnPageChangeListener { - - override fun onChapterChange(pos: Int) { - var index: Int = pos - if (pos >= mChapters.size) { - index = mChapters.size - 1 - } - mCategoryAdapter.setChapter(index) - mCurrentChapter = mChapters[index] - currentChapter = index - } - - override fun requestChapters(requestChapters: List) { - AccountManager.getInstance().getBookArticleDetail(mBookId, requestChapters) - mHandler.sendEmptyMessage(WHAT_CATEGORY) - } - - override fun onCategoryFinish(chapters: List) { - mChapters.clear() - mChapters.addAll(chapters) - mCategoryAdapter.refreshItems(mChapters) - } - - override fun onPageCountChange(count: Int) {} - - override fun onPageChange(pos: Int) { - - } - } - ) - read_pv_page.setTouchListener(object : PageView.TouchListener { - override fun onTouch(): Boolean { - return !hideReadMenu() - } - - override fun center() { - toggleMenu(true) - } - - override fun prePage() {} - - override fun nextPage() {} - - override fun cancel() {} - }) - read_tv_category.setOnClickListener { - //移动到指定位置 - if (mCategoryAdapter.count > 0) { - rlv_list.setSelection(mPageLoader.chapterPos) - } - //切换菜单 - toggleMenu(true) - //打开侧滑动栏 - read_dl_slide.openDrawer(GravityCompat.START) - } - tv_light.setOnClickListener { - ll_light.visibility = GONE - rlReadMark.visibility = GONE - if (isVisible(ll_light)) { - ll_light.visibility = GONE - } else { - ll_light.visibility = VISIBLE - } - } - tv_setting.setOnClickListener { - ll_light.visibility = GONE - rlReadMark.visibility = GONE - toggleMenu(false) - mSettingDialog.show() - } - - read_setting_sb_brightness.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - val progress = seekBar.progress - //设置当前 Activity 的亮度 - BrightnessUtils.setBrightness(this@NovelReadActivity, progress) - //存储亮度的进度条 - ReadSettingManager.getInstance().brightness = progress - } - }) - - tvBookReadMode.setOnClickListener { - isNightMode = !isNightMode - mPageLoader.setNightMode(isNightMode) - toggleNightMode() - } - - read_tv_brief.setOnClickListener { - val intent = Intent(this, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, Integer.valueOf(mBookId)) - startActivity(intent) - } - - read_tv_community.setOnClickListener { - if (isVisible(read_ll_bottom_menu)) { - if (isVisible(rlReadMark)) { - gone(rlReadMark) - } else { - gone(ll_light) - updateMark() - visible(rlReadMark) - } - } - } - - tvAddMark.setOnClickListener { - mMarkAdapter.edit = false - if (BookRepository.getInstance().getSignById(mCurrentChapter.chapterId)) { - showToast(getString(R.string.sign_exist)) - return@setOnClickListener - } - BookRepository.getInstance().addSign(mBookId, mCurrentChapter.chapterId, mCurrentChapter.title) - updateMark() - } - - tvClear.setOnClickListener { - if (mMarkAdapter.edit) { - val sign = mMarkAdapter.selectList - if (sign != "") { - BookRepository.getInstance().deleteSign(sign) - updateMark() - } - mMarkAdapter.edit = false - } else { - mMarkAdapter.edit = true - } - } - - tv_cache.setOnClickListener { - if (!isCollected) { //没有收藏 先收藏 然后弹框 - //设置为已收藏 - isCollected = true - //设置阅读时间 - mCollBook.lastRead = System.currentTimeMillis().toString() - BookRepository.getInstance().saveCollBookWithAsync(mCollBook) - } - showDownLoadDialog() - - } - rlv_list.setOnItemClickListener { _, _, position, _ -> - read_dl_slide.closeDrawer(GravityCompat.START) - mPageLoader.skipToChapter(position) - } - iv_guide.setOnClickListener { - iv_guide.visibility = GONE - SpUtil.setBooleanValue(Constant.BookGuide, true) - } - } - - private fun showDownLoadDialog() { - val builder = AlertDialog.Builder(this) - builder.setTitle(getString(R.string.d_cache_num)) - .setItems( - arrayOf( - getString(R.string.d_cache_last_50), - getString(R.string.d_cache_last_all), - getString(R.string.d_cache_all) - ) - ) { _, which -> - when (which) { - 0 -> { - //50章 - val last = currentChapter + 50 - if (last > mCollBook.bookChapters.size) { - downLoadCache(mCollBook.bookChapters, mCollBook.bookChapters.size) - } else { - downLoadCache(mCollBook.bookChapters, last) - } - } - 1 -> { - //后面所有 - val lastBeans = ArrayList() - for (i in currentChapter until mCollBook.bookChapters.size) { - lastBeans.add(mCollBook.bookChapters[i]) - } - downLoadCache(lastBeans, mCollBook.bookChapters.size - currentChapter) - } - 2 -> downLoadCache(mCollBook.bookChapters, mCollBook.bookChapters.size) //所有 - else -> { - } - } - toggleMenu(true) - } - builder.show() - } - - private fun downLoadCache(beans: List, size: Int) { - val task = DownloadTaskBean() - task.taskName = mCollBook.title - task.bookId = mCollBook.id - task.bookChapters = beans //计算要缓存的章节 - task.currentChapter = currentChapter - task.lastChapter = size - - RxBus.getInstance().post(task) - startService(Intent(this, DownloadService::class.java)) - } - - private fun toggleNightMode() { - if (isNightMode) { - tvBookReadMode.text = resources.getString(R.string.book_read_mode_day) - val drawable = ContextCompat.getDrawable(this, R.drawable.ic_read_menu_moring) - tvBookReadMode.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null) - cl_layout.setBackgroundColor(ContextCompat.getColor(this, R.color.read_bg_night)) - } else { - tvBookReadMode.text = resources.getString(R.string.book_read_mode_day) - val drawable = ContextCompat.getDrawable(this, R.drawable.ic_read_menu_night) - tvBookReadMode.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null) - cl_layout.setBackgroundColor( - ContextCompat.getColor( - this, - ReadSettingManager.getInstance().pageStyle.bgColor - ) - ) - } - } - - /** - * 隐藏阅读界面的菜单显示 - * - * @return 是否隐藏成功 - */ - private fun hideReadMenu(): Boolean { - hideSystemBar() - if (read_abl_top_menu.visibility == VISIBLE) { - toggleMenu(true) - return true - } else if (mSettingDialog.isShowing) { - mSettingDialog.dismiss() - return true - } - return false - } - - private fun showSystemBar() { - //显示 - SystemBarUtils.showUnStableStatusBar(this) - if (isFullScreen) { - SystemBarUtils.showUnStableNavBar(this) - } - } - - private fun hideSystemBar() { - //隐藏 - SystemBarUtils.hideStableStatusBar(this) - if (isFullScreen) { - SystemBarUtils.hideStableNavBar(this) - } - } - - /** - * 切换菜单栏的可视状态 - * 默认是隐藏的 - */ - private fun toggleMenu(hideStatusBar: Boolean) { - initMenuAnim() - gone(ll_light, rlReadMark) - if (read_abl_top_menu.visibility == VISIBLE) { - //关闭 - read_abl_top_menu.startAnimation(mTopOutAnim) - read_ll_bottom_menu.startAnimation(mBottomOutAnim) - read_abl_top_menu.visibility = GONE - read_ll_bottom_menu.visibility = GONE - - if (hideStatusBar) { - hideSystemBar() - } - } else { - read_abl_top_menu.visibility = VISIBLE - read_ll_bottom_menu.visibility = VISIBLE - read_abl_top_menu.startAnimation(mTopInAnim) - read_ll_bottom_menu.startAnimation(mBottomInAnim) - - showSystemBar() - } - } - - //初始化菜单动画 - private fun initMenuAnim() { - if (mTopInAnim != null) return - mTopInAnim = AnimationUtils.loadAnimation(this, R.anim.slide_top_in) - mTopOutAnim = AnimationUtils.loadAnimation(this, R.anim.slide_top_out) - mBottomInAnim = AnimationUtils.loadAnimation(this, R.anim.slide_bottom_in) - mBottomOutAnim = AnimationUtils.loadAnimation(this, R.anim.slide_bottom_out) - //退出的速度要快 - mTopOutAnim!!.duration = 200 - mBottomOutAnim!!.duration = 200 - } - - @Subscribe - fun getBookArticle(event: BookArticleEvent) { - if (event.isFail) { - //获取章节失败处理 - Log.e(TAG, event.er!!.msg) - } else { - val chapterBeans = event.result!!.chapterBean - mPageLoader.collBook.bookChapters = chapterBeans - mPageLoader.refreshChapterList() - - // 如果是目录更新的情况,那么就需要存储更新数据 - if (mCollBook.isUpdate && isCollected) { - BookRepository.getInstance().saveBookChaptersWithAsync(chapterBeans, mCollBook) - } - } - } - - @Subscribe - fun finishChapter(event: FinishChapterEvent) { - if (mPageLoader.pageStatus == PageLoader.STATUS_LOADING) { - mHandler.sendEmptyMessage(WHAT_CHAPTER) - } - // 当完成章节的时候,刷新列表 - mCategoryAdapter.notifyDataSetChanged() - } - - @Subscribe - fun errorChapter(event: ErrorChapterEvent) { - if (mPageLoader.pageStatus == PageLoader.STATUS_LOADING) { - mPageLoader.chapterError() - } - } - - private fun updateMark() { - mMarks.clear() - mMarks.addAll(BookRepository.getInstance().getSign(mBookId)) - mMarkAdapter.notifyDataSetChanged() - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - when (keyCode) { - KeyEvent.KEYCODE_VOLUME_UP -> return mPageLoader.skipToPrePage() - - KeyEvent.KEYCODE_VOLUME_DOWN -> return mPageLoader.skipToNextPage() - } - return super.onKeyDown(keyCode, event) - } - - override fun onBackPressed() { - if (read_abl_top_menu.visibility == VISIBLE) { - // 非全屏下才收缩,全屏下直接退出 - if (!ReadSettingManager.getInstance().isFullScreen) { - toggleMenu(true) - return - } - } else if (mSettingDialog.isShowing) { - mSettingDialog.dismiss() - return - } else if (read_dl_slide.isDrawerOpen(GravityCompat.START)) { - read_dl_slide.closeDrawer(GravityCompat.START) - return - } - Log.e(TAG, "onBackPressed: " + mCollBook.bookChapters.isEmpty()) - - if (!mCollBook.isLocal && !isCollected && mCollBook.bookChapters.isNotEmpty()) { - val alertDialog = AlertDialog.Builder(this) - .setTitle(getString(R.string.add_book)) - .setMessage(getString(R.string.like_book)) - .setPositiveButton(getString(R.string.sure)) { dialog, which -> - //设置为已收藏 - isCollected = true - //设置阅读时间 - mCollBook.lastRead = System.currentTimeMillis().toString() - - BookRepository.getInstance().saveCollBookWithAsync(mCollBook) - - exit() - } - .setNegativeButton(getString(R.string.cancel)) { dialog, which -> exit() }.create() - alertDialog.show() - } else { - exit() - } - } - - // 退出 - private fun exit() { - // 返回给BookDetail。 - val result = Intent() - result.putExtra(RESULT_IS_COLLECTED, isCollected) - setResult(Activity.RESULT_OK, result) - super.onBackPressed() - } - - override fun onPause() { - super.onPause() - if (isCollected) { - mPageLoader.saveRecord() - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - mPageLoader.closeBook() - unbindService(mConn) - unregisterReceiver(mReceiver) - } - - private fun initService() { - - mConn = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, service: IBinder) { - mService = service as DownloadService.IDownloadManager - mService!!.setOnDownloadListener(this@NovelReadActivity) - } - - override fun onServiceDisconnected(name: ComponentName) {} - } - //绑定 - bindService(Intent(this, DownloadService::class.java), mConn, Service.BIND_AUTO_CREATE) - } - - override fun onDownloadChange(pos: Int, status: Int, msg: String) { - Log.e(TAG, "onDownloadChange: $pos $status $msg") - - if (msg == getString(R.string.download_success) || msg == getString(R.string.download_error)) { - //下载成功或失败后隐藏下载视图 - if (ll_download != null) { - ll_download.visibility = GONE - showToast(msg) - } - } else { - if (ll_download != null) { - ll_download.visibility = VISIBLE - tv_progress.text = getString( - R.string.download_loading, - mService!!.downloadTaskList[pos].currentChapter, - mService!!.downloadTaskList[pos].lastChapter - ) - pb_loading.max = mService!!.downloadTaskList[pos].lastChapter - pb_loading.progress = mService!!.downloadTaskList[pos].currentChapter - } - } - } - - - override fun onDownloadResponse(pos: Int, status: Int) { - Log.e(TAG, "onDownloadResponse: $pos $status") - } - - @Subscribe - fun onDownLoadEvent(message: DownloadMessage) { - showToast(message.message) - } - - companion object { - - private const val TAG = "NovelReadActivity" - const val EXTRA_COLL_BOOK = "extra_coll_book" - const val EXTRA_IS_COLLECTED = "extra_is_collected" - private const val WHAT_CATEGORY = 1 - private const val WHAT_CHAPTER = 2 - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelRecommendBookListActivity.kt b/app/src/main/java/com/novel/read/activity/NovelRecommendBookListActivity.kt deleted file mode 100644 index da0c2b4..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelRecommendBookListActivity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.novel.read.activity - -import android.view.View -import androidx.fragment.app.Fragment -import androidx.viewpager.widget.ViewPager -import com.novel.read.R -import com.novel.read.adapter.ViewPageAdapter -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.fragment.BookListFragment -import com.novel.read.widget.VpTabLayout -import kotlinx.android.synthetic.main.activity_recommend_book_list.* -import java.util.* - -class NovelRecommendBookListActivity(override val layoutId: Int= R.layout.activity_recommend_book_list) : NovelBaseActivity() { - - override fun initView() { - val fragmentList = ArrayList() - val sex = intent.getStringExtra(Constant.Sex) - val type = intent.getStringExtra(Constant.Type) - when (type) { - Constant.ListType.Human -> toolbar.title = getString(R.string.popular_selection) - Constant.ListType.EditRecommend -> toolbar.title = getString(R.string.edit_recommend) - Constant.ListType.HotSearch -> toolbar.title = getString(R.string.hot_search) - } - val generalFragment = BookListFragment.newInstance(type, Constant.DateTyp.General, sex) - val monthFragment = BookListFragment.newInstance(type, Constant.DateTyp.Month, sex) - val weekFragment = BookListFragment.newInstance(type, Constant.DateTyp.Week, sex) - fragmentList.add(generalFragment) - fragmentList.add(monthFragment) - fragmentList.add(weekFragment) - val pageAdapter = ViewPageAdapter(supportFragmentManager, fragmentList) - vp_recommend_type.adapter = pageAdapter - vp_recommend_type.offscreenPageLimit = 2 - vp_recommend_type.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - - } - - override fun onPageSelected(position: Int) { - vp_tab.setAnim(position, vp_recommend_type) - } - - override fun onPageScrollStateChanged(state: Int) {} - }) - } - - override fun initData() { - toolbar!!.setNavigationOnClickListener { finish() } - - vp_tab.setOnTabBtnClickListener(object : VpTabLayout.OnTabClickListener { - override fun onTabBtnClick(var1: VpTabLayout.CommonTabBtn, var2: View) { - when (var1) { - VpTabLayout.CommonTabBtn.ONE -> vp_tab.setAnim(0, vp_recommend_type) - VpTabLayout.CommonTabBtn.TWO -> vp_tab.setAnim(1, vp_recommend_type) - VpTabLayout.CommonTabBtn.THREE -> vp_tab.setAnim(2, vp_recommend_type) - } - } - }) - - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelSearchActivity.kt b/app/src/main/java/com/novel/read/activity/NovelSearchActivity.kt deleted file mode 100644 index 52a935a..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelSearchActivity.kt +++ /dev/null @@ -1,255 +0,0 @@ -package com.novel.read.activity - -import android.content.DialogInterface -import android.text.Editable -import android.text.TextUtils -import android.text.TextWatcher -import android.view.KeyEvent -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.flexbox.AlignItems -import com.google.android.flexbox.FlexDirection -import com.google.android.flexbox.FlexWrap -import com.google.android.flexbox.FlexboxLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.adapter.HistoryAdapter -import com.novel.read.adapter.HotAdapter -import com.novel.read.adapter.SearchAdapter -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant.COMMENT_SIZE -import com.novel.read.event.HotSearchEvent -import com.novel.read.event.SearchListEvent -import com.novel.read.http.AccountManager -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.db.SearchListTable -import com.novel.read.model.protocol.SearchResp -import com.novel.read.utlis.DialogUtils -import com.spreada.utils.chinese.ZHConverter -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.activity_search.* -import kotlinx.android.synthetic.main.title_search.* -import org.litepal.LitePal -import java.util.* - -class NovelSearchActivity(override val layoutId: Int= R.layout.activity_search) : NovelBaseActivity() { - - private val mHotList = ArrayList() - private lateinit var mHotAdapter: HotAdapter - - private var mHisList: MutableList = ArrayList() - private lateinit var mHisAdapter: HistoryAdapter - - private val mSearchList = ArrayList() - private lateinit var mSearchAdapter: SearchAdapter - - private var page = 1 - private var loadSize: Int = 0 - - override fun initView() { - EventManager.instance.registerSubscriber(this) - - val manager = FlexboxLayoutManager(this) - //设置主轴排列方式 - manager.flexDirection = FlexDirection.ROW - //设置是否换行 - manager.flexWrap = FlexWrap.WRAP - manager.alignItems = AlignItems.STRETCH - rlv_hot.layoutManager = manager - mHotAdapter = HotAdapter(mHotList) - rlv_hot.adapter = mHotAdapter - - mHisList = LitePal.order("saveTime desc").limit(5).find(SearchListTable::class.java) - val manager2 = FlexboxLayoutManager(this) - //设置主轴排列方式 - manager2.flexDirection = FlexDirection.ROW - //设置是否换行 - manager2.flexWrap = FlexWrap.WRAP - manager2.alignItems = AlignItems.STRETCH - mHisAdapter = HistoryAdapter(mHisList) - rlv_history.layoutManager = manager2 - rlv_history.adapter = mHisAdapter - - rlv_search.layoutManager = LinearLayoutManager(this) - mSearchAdapter = SearchAdapter(mSearchList, rlv_search) - rlv_search.adapter = mSearchAdapter - - mSearchAdapter.setOnLoadMoreListener(object : OnLoadMoreListener { - override fun onLoadMore() { - if (mSearchAdapter.isLoadingMore) { - - } else { - if (loadSize >= COMMENT_SIZE) { - mSearchAdapter.isLoadingMore = true - mSearchAdapter.notifyDataSetChanged() - page++ - getData() - } - } - } - }) - - AccountManager.getInstance().getHotSearch() - } - - private fun getData() { - val str = convertCC(tv_search.text.toString().trim { it <= ' ' }) - AccountManager.getInstance().getSearchBookList("", str, page) - } - - //繁簡轉換 - fun convertCC(input: String): String { - return if (TextUtils.isEmpty(input) || input.isEmpty()) "" else ZHConverter.getInstance( - ZHConverter.SIMPLIFIED - ).convert(input) - } - - override fun initData() { - //输入框 - tv_search.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - - } - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (s.toString().trim { it <= ' ' } == "") { - refresh.visibility = View.GONE - head_hot.visibility = View.VISIBLE - head_history.visibility = View.VISIBLE - rlv_hot.visibility = View.VISIBLE - rlv_history.visibility = View.VISIBLE - } else { - refresh.visibility = View.VISIBLE - head_hot.visibility = View.GONE - head_history.visibility = View.GONE - rlv_hot.visibility = View.GONE - rlv_history.visibility = View.GONE - refresh.showLoading() - page = 1 - getData() - } - - } - - override fun afterTextChanged(s: Editable) { - - } - }) - - //键盘的搜索 - tv_search.setOnKeyListener { v, keyCode, event -> - //修改回车键功能 - if (keyCode == KeyEvent.KEYCODE_ENTER) { - mSearchAdapter.setHolderType(true) - saveKey() - return@setOnKeyListener true - } - false - } - - mHotAdapter.setOnItemClickListener(object :HotAdapter.OnItemClickListener{ - override fun onItemClick(view: View, pos: Int) { - mSearchAdapter.setHolderType(true) - refresh.visibility = View.VISIBLE - tv_search.setText(mHotList[pos]) - saveKey() - } - }) - - mHisAdapter.setOnItemClickListener(object :HistoryAdapter.OnItemClickListener{ - override fun onItemClick(view: View, pos: Int) { - mSearchAdapter.setHolderType(true) - refresh.visibility = View.VISIBLE - tv_search.setText(mHisList[pos].key) - saveKey() - } - }) - - mSearchAdapter.setOnItemClickListener(object :SearchAdapter.OnItemClickListener{ - override fun onItemClick(view: View, pos: Int) { - mSearchAdapter.setHolderType(true) - tv_search.setText(mSearchList[pos].title) - saveKey() - } - }) - - head_history.setOnClickListener { view -> - - DialogUtils.getInstance().showAlertDialog( - this, - getString(R.string.clear_search), - dialogListener = DialogInterface.OnClickListener { dialog, which -> - LitePal.deleteAll(SearchListTable::class.java) - mHisList.clear() - mHisList.addAll(LitePal.order("saveTime desc").limit(5).find(SearchListTable::class.java)) - mHisAdapter.notifyDataSetChanged() - }) - } - - tv_cancel.setOnClickListener { - onBackPressed() - } - - refresh.setOnReloadingListener { getData() } - - } - - private fun saveKey() { - if (tv_search.text.toString().trim { it <= ' ' } == "") { - return - } - val searchListTable = SearchListTable() - searchListTable.key = tv_search.text.toString().trim { it <= ' ' } - searchListTable.saveTime = System.currentTimeMillis() - searchListTable.saveOrUpdate("key=?", tv_search.text.toString().trim { it <= ' ' }) - mHisList.clear() - mHisList.addAll(LitePal.order("saveTime desc").limit(5).find(SearchListTable::class.java)) - mHisAdapter.notifyDataSetChanged() - } - - override fun onBackPressed() { - if (refresh.visibility == View.VISIBLE) { - tv_search.setText("") - mSearchAdapter.setHolderType(false) - page = 1 - } else { - super.onBackPressed() - overridePendingTransition(R.anim.message_fade_in, R.anim.message_fade_out) - } - } - - @Subscribe - fun getHotSearch(event: HotSearchEvent) { - if (event.isFail) { - - } else { - mHotList.clear() - mHotList.addAll(event.result!!.key) - mHotAdapter.notifyDataSetChanged() - } - } - - @Subscribe - fun getSearchList(event: SearchListEvent) { - refresh.showFinish() - if (event.isFail) { - refresh.showError() - } else { - loadSize = event.result!!.book.size - if (mSearchAdapter.isLoadingMore) { - mSearchAdapter.isLoadingMore = false - mSearchList.addAll(event.result!!.book) - mSearchAdapter.notifyDataSetChanged() - } else { - mSearchList.clear() - mSearchList.addAll(event.result!!.book) - mSearchAdapter.notifyDataSetChanged() - } - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelSettingActivity.kt b/app/src/main/java/com/novel/read/activity/NovelSettingActivity.kt deleted file mode 100644 index 33a1667..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelSettingActivity.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.novel.read.activity - -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.text.TextUtils -import android.view.View -import com.allenliu.versionchecklib.v2.AllenVersionChecker -import com.allenliu.versionchecklib.v2.builder.UIData -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.base.NovelBaseActivity -import com.novel.read.constants.Constant -import com.novel.read.event.UpdateBookEvent -import com.novel.read.event.VersionEvent -import com.novel.read.http.AccountManager -import com.novel.read.model.protocol.VersionResp -import com.novel.read.utlis.CleanCacheUtils -import com.novel.read.utlis.LocalManageUtil -import com.novel.read.utlis.SpUtil -import com.novel.read.utlis.VersionUtil -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.activity_setting.* - -class NovelSettingActivity(override val layoutId: Int = R.layout.activity_setting) : NovelBaseActivity(), View.OnClickListener { - - private var resp: VersionResp? = null - - override fun initView() { - EventManager.instance.registerSubscriber(this) - } - - @SuppressLint("SetTextI18n") - override fun initData() { - tv_language.text = - resources.getStringArray(R.array.setting_dialog_language_choice)[SpUtil.getIntValue( - Constant.Language, - 1 - )] - tv_version.text = "V" + VersionUtil.getPackageName(this)!! - try { - val cacheSize = - CleanCacheUtils.getInstance().getTotalCacheSize(this@NovelSettingActivity) - tv_cache_num.text = cacheSize - } catch (e: Exception) { - e.printStackTrace() - } - toolbar.setNavigationOnClickListener { finish() } - AccountManager.getInstance().checkVersion(VersionUtil.getPackageCode(this)) - - ll_choose_language.setOnClickListener(this) - ll_clear_cache.setOnClickListener(this) - tv_check.setOnClickListener(this) - } - - override fun onClick(v: View?) { - when (v?.id) { - R.id.ll_choose_language -> showLanguageDialog() - R.id.ll_clear_cache -> { - //默认不勾选清空书架列表,防手抖!! - val selected = booleanArrayOf(true, false) - AlertDialog.Builder(this) - .setTitle(getString(R.string.clear_cache)) - .setCancelable(true) - .setMultiChoiceItems(arrayOf(getString(R.string.clear_cache), getString(R.string.clear_book)), selected) { _, which, isChecked -> selected[which] = isChecked } - .setPositiveButton(getString(R.string.sure)) { dialog, _ -> - Thread { - CleanCacheUtils.getInstance() - .clearCache(selected[0], selected[1], this@NovelSettingActivity) - var cacheSize = "" - try { - cacheSize = - CleanCacheUtils.getInstance() - .getTotalCacheSize(this@NovelSettingActivity) - } catch (e: Exception) { - e.printStackTrace() - } - val finalCacheSize = cacheSize - runOnUiThread { - EventManager.instance.postEvent(UpdateBookEvent()) - tv_cache_num.text = finalCacheSize - } - }.start() - dialog.dismiss() - } - .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.dismiss() } - .create().show() - } - R.id.tv_check -> - //版本大小不为空 去更新。 - updateApk(resp!!) - } - } - - private fun showLanguageDialog() { - AlertDialog.Builder(this) - .setTitle(getString(R.string.choose_language)) - .setSingleChoiceItems( - resources.getStringArray(R.array.setting_dialog_language_choice), - SpUtil.getIntValue(Constant.Language, 1) - ) { dialog, which -> - val language = - resources.getStringArray(R.array.setting_dialog_language_choice)[which] - tv_language.text = language - SpUtil.setIntValue(Constant.Language, which) - dialog.dismiss() - - if (which == 0) { - selectLanguage(0) - } else { - selectLanguage(1) - } - } - .create().show() - } - - private fun selectLanguage(select: Int) { - LocalManageUtil.saveSelectLanguage(this, select) - NovelMainActivity.reStart(this) - } - - @Subscribe - fun checkVersion(event: VersionEvent) { - if (event.isFail) { - - } else { - if (TextUtils.isEmpty(event.result!!.version.size)) { - return - } - resp = event.result - tv_check.visibility = View.VISIBLE - - } - } - - private fun updateApk(resp: VersionResp) { - val versionBean = resp.version - val builder = AllenVersionChecker - .getInstance() - .downloadOnly( - UIData.create() - .setTitle(getString(R.string.new_version, versionBean.version)) - .setContent(versionBean.content) - .setDownloadUrl(versionBean.download) - ) - builder.executeMission(this) - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } -} diff --git a/app/src/main/java/com/novel/read/activity/NovelSplashActivity.kt b/app/src/main/java/com/novel/read/activity/NovelSplashActivity.kt deleted file mode 100644 index ee7f81f..0000000 --- a/app/src/main/java/com/novel/read/activity/NovelSplashActivity.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.novel.read.activity - -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import com.novel.read.R -import com.novel.read.utlis.PermissionUtil -import com.novel.read.utlis.StatusBarUtil -import kotlinx.android.synthetic.main.activity_splash.* - -class NovelSplashActivity : AppCompatActivity(), PermissionUtil.PermissionCallBack { - - private var flag = false - private var runnable: Runnable? = null - - private var mPermissionUtil: PermissionUtil = PermissionUtil.getInstance() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - StatusBarUtil.setBarsStyle(this, R.color.colorPrimary, true) - setContentView(R.layout.activity_splash) - mPermissionUtil.requestPermissions(this, PERMISSION_CODE, this) - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - mPermissionUtil.requestResult(this, permissions, grantResults, this) - } - - private fun init() { - runnable = Runnable { goHome() } - tvSkip.postDelayed(runnable, 2000) - tvSkip.setOnClickListener { goHome() } - } - - @Synchronized - private fun goHome() { - if (!flag) { - flag = true - startActivity(Intent(this, NovelMainActivity::class.java)) - finish() - } - } - - override fun onDestroy() { - super.onDestroy() - flag = true - tvSkip.removeCallbacks(runnable) - } - - override fun onPermissionSuccess() { - init() - } - - override fun onPermissionReject(strMessage: String) { - finish() - } - - override fun onPermissionFail() { - mPermissionUtil.requestPermissions(this, PERMISSION_CODE, this) - } - - companion object { - private val PERMISSION_CODE = 999 - } -} diff --git a/app/src/main/java/com/novel/read/adapter/BookAdapter.kt b/app/src/main/java/com/novel/read/adapter/BookAdapter.kt deleted file mode 100644 index 53694ca..0000000 --- a/app/src/main/java/com/novel/read/adapter/BookAdapter.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelReadActivity -import com.novel.read.model.db.CollBookBean -import com.novel.read.utlis.GlideImageLoader - -import java.util.ArrayList - -class BookAdapter(private val mList: List) : RecyclerView.Adapter() { - private lateinit var mContext: Context - - private var mClickListener: OnItemClickListener? = null - private var mEdit: Boolean = false - - val selectList: List - get() { - val collBookBeans = ArrayList() - for (i in mList.indices) { - if (mList[i].isSelect) { - collBookBeans.add(mList[i]) - } - } - return collBookBeans - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder { - mContext = viewGroup.context - val view: View - if (i == VALUE_ITEM) { - view = LayoutInflater.from(mContext).inflate(R.layout.rlv_item_book, viewGroup, false) - return ViewHolder(view) - } else if (i == EMPTY_ITEM) { - view = LayoutInflater.from(mContext).inflate(R.layout.rlv_empty_add_book, viewGroup, false) - return EmptyHolder(view) - } - throw IllegalArgumentException() - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - if (viewHolder is ViewHolder) { - if (mEdit) { //编辑模式 - viewHolder.mIvCheck.isSelected = mList[i].isSelect - viewHolder.mIvCheck.visibility = View.VISIBLE - viewHolder.mTvBookName.text = mList[i].title - viewHolder.mTvBookAuthor.text = mList[i].author - if (mList[i].isUpdate) { - viewHolder.mIvGeng.visibility = View.VISIBLE - } else { - viewHolder.mIvGeng.visibility = View.GONE - } - GlideImageLoader.displayCornerImage(mContext, mList[i].cover, viewHolder.mIvBook) - viewHolder.itemView.setOnClickListener { - viewHolder.mIvCheck.isSelected = !mList[i].isSelect - mList[i].isSelect = !mList[i].isSelect - } - } else { //正常模式 - viewHolder.mIvCheck.visibility = View.GONE - if (mList.size == i) { //最后的条目 - viewHolder.mTvBookName.text = "" - viewHolder.mTvBookAuthor.text = "" - viewHolder.mIvBook.setImageResource(R.drawable.ic_book_add) - viewHolder.mIvGeng.visibility = View.GONE - viewHolder.itemView.setOnClickListener { view -> - mClickListener!!.onItemClick( - view, - i - ) - } - } else { - viewHolder.mTvBookName.text = mList[i].title - viewHolder.mTvBookAuthor.text = mList[i].author - if (mList[i].isUpdate) { - viewHolder.mIvGeng.visibility = View.VISIBLE - } else { - viewHolder.mIvGeng.visibility = View.GONE - } - GlideImageLoader.displayCornerImage( - mContext, - mList[i].cover, - viewHolder.mIvBook - ) - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelReadActivity::class.java) - intent.putExtra(NovelReadActivity.EXTRA_IS_COLLECTED, true) - intent.putExtra(NovelReadActivity.EXTRA_COLL_BOOK, mList[i]) - mContext.startActivity(intent) - } - } - } - } else if (viewHolder is EmptyHolder) { //空条目 - viewHolder.mBtnAdd.setOnClickListener { view -> mClickListener!!.onItemClick(view, i) } - } - } - - override fun getItemCount(): Int { - if (mList.isEmpty()) { - return 1 - } - return if (mEdit) { - mList.size - } else { - mList.size + 1 - } - } - - override fun getItemViewType(position: Int): Int { - return if (mList.isEmpty()) { - EMPTY_ITEM - } else { - VALUE_ITEM - } - - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mIvBook: ImageView = itemView.findViewById(R.id.iv_book) - var mTvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var mTvBookAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - var mIvCheck: ImageView = itemView.findViewById(R.id.iv_check) - var mIvGeng: ImageView = itemView.findViewById(R.id.iv_geng) - - } - - internal class EmptyHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mBtnAdd: Button = itemView.findViewById(R.id.btn_add) - - } - - fun setOnItemClickListener(mListener: OnItemClickListener) { - this.mClickListener = mListener - } - - - interface OnItemClickListener { - fun onItemClick(view: View, pos: Int) - } - - fun setEdit(edit: Boolean) { //开启编辑模式 - mEdit = edit - notifyDataSetChanged() - } - - - companion object { - - private const val VALUE_ITEM = 100 //正常item - - private const val EMPTY_ITEM = 101 //空白item - - } - -} diff --git a/app/src/main/java/com/novel/read/adapter/BookListAdapter.kt b/app/src/main/java/com/novel/read/adapter/BookListAdapter.kt deleted file mode 100644 index 5c0ec4f..0000000 --- a/app/src/main/java/com/novel/read/adapter/BookListAdapter.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.adapter.holder.EmptyHolder -import com.novel.read.adapter.holder.MoreHolder -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.COMMENT_SIZE -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.protocol.SearchResp -import com.novel.read.utlis.GlideImageLoader - -class BookListAdapter(private val mList: List, recyclerView: RecyclerView) : RecyclerView.Adapter() { - private var mContext: Context? = null - - var isLoadingMore: Boolean = false - private var lastVisibleItem: Int = 0 - private var totalItemCount: Int = 0 - private val visibleThreshold = 1 - private var mOnLoadMoreListener: OnLoadMoreListener? = null - - init { - if (recyclerView.layoutManager is LinearLayoutManager) { - val manager = recyclerView.layoutManager as LinearLayoutManager? - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - totalItemCount = manager!!.itemCount - lastVisibleItem = manager.findLastVisibleItemPosition() - if (!isLoadingMore && totalItemCount == lastVisibleItem + visibleThreshold && totalItemCount >= COMMENT_SIZE) { - if (mOnLoadMoreListener != null) { - mOnLoadMoreListener!!.onLoadMore() - } - } - } - }) - - } - } - - fun setOnLoadMoreListener(listener: OnLoadMoreListener) { - this.mOnLoadMoreListener = listener - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view: View - return when (viewType) { - VALUE_ITEM -> { - view = LayoutInflater.from(mContext).inflate(R.layout.rlv_item_book_list, viewGroup, false) - ViewHolder(view) - } - EMPTY_ITEM -> { - view = LayoutInflater.from(mContext).inflate(R.layout.rlv_empty_view, viewGroup, false) - EmptyHolder(view) - } - PROCESS_ITEM -> { - view = LayoutInflater.from(mContext).inflate(R.layout.load_more_layout, viewGroup, false) - MoreHolder(view) - } - else -> throw IllegalArgumentException() - } - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - if (viewHolder is ViewHolder) { - val bookBean = mList[i] - viewHolder.tvBookName.text = bookBean.title - viewHolder.tvBookAuthor.text = bookBean.author - viewHolder.tvBookDescription.text = bookBean.description - GlideImageLoader.displayCornerImage(mContext!!, bookBean.cover!!, viewHolder.ivBook) - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, bookBean.id) - mContext!!.startActivity(intent) - } - }else if (viewHolder is MoreHolder){ - viewHolder.bindModule(isLoadingMore) - } - } - - override fun getItemCount(): Int { - return if (mList.isEmpty()) { - 1 - } else mList.size + 1 - } - - override fun getItemViewType(position: Int): Int { - if (mList.isNotEmpty() &&position == itemCount - 1) { - return PROCESS_ITEM - } - return when { - mList.isEmpty() -> EMPTY_ITEM - else -> VALUE_ITEM - } - - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var ivBook: ImageView = itemView.findViewById(R.id.iv_book) - var tvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var tvBookAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - var tvBookDescription: TextView = itemView.findViewById(R.id.tv_book_description) - } - - companion object { - - private const val VALUE_ITEM = 100 //正常item - - private const val EMPTY_ITEM = 101 //空白item - - private const val PROCESS_ITEM = 102 - } - -} diff --git a/app/src/main/java/com/novel/read/adapter/CategoryAdapter.java b/app/src/main/java/com/novel/read/adapter/CategoryAdapter.java deleted file mode 100644 index 42f3b12..0000000 --- a/app/src/main/java/com/novel/read/adapter/CategoryAdapter.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.novel.read.adapter; - -import android.view.View; -import android.view.ViewGroup; - -import com.novel.read.adapter.holder.CategoryHolder; -import com.novel.read.widget.page.TxtChapter; - -public class CategoryAdapter extends EasyAdapter { - private int currentSelected = 0; - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - CategoryHolder holder = (CategoryHolder) view.getTag(); - - if (position == currentSelected){ - holder.setSelectedChapter(); - } - - return view; - } - - @Override - protected IViewHolder onCreateViewHolder(int viewType) { - return new CategoryHolder(); - } - - public void setChapter(int pos){ - currentSelected = pos; - notifyDataSetChanged(); - } - -} diff --git a/app/src/main/java/com/novel/read/adapter/EasyAdapter.java b/app/src/main/java/com/novel/read/adapter/EasyAdapter.java deleted file mode 100644 index 36a3bb5..0000000 --- a/app/src/main/java/com/novel/read/adapter/EasyAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.novel.read.adapter; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public abstract class EasyAdapter extends BaseAdapter { - - private List mList = new ArrayList(); - - @Override - public int getCount() { - return mList.size(); - } - - @Override - public T getItem(int position) { - return mList.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - public void addItem(T value){ - mList.add(value); - notifyDataSetChanged(); - } - - public void addItem(int index,T value){ - mList.add(index, value); - notifyDataSetChanged(); - } - - public void addItems(List values){ - mList.addAll(values); - notifyDataSetChanged(); - } - - public void removeItem(T value){ - mList.remove(value); - notifyDataSetChanged(); - } - - public List getItems(){ - return Collections.unmodifiableList(mList); - } - - public int getItemSize(){ - return mList.size(); - } - - public void refreshItems(List list){ - mList.clear(); - mList.addAll(list); - notifyDataSetChanged(); - } - - public void clear(){ - mList.clear(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - IViewHolder holder; - if (convertView == null){ - holder = onCreateViewHolder(getItemViewType(position)); - convertView = holder.createItemView(parent); - convertView.setTag(holder); - //初始化 - holder.initView(); - } - else { - holder = (IViewHolder)convertView.getTag(); - } - //执行绑定 - holder.onBind(getItem(position),position); - return convertView; - } - - protected abstract IViewHolder onCreateViewHolder(int viewType); -} diff --git a/app/src/main/java/com/novel/read/adapter/EditRecommendAdapter.kt b/app/src/main/java/com/novel/read/adapter/EditRecommendAdapter.kt deleted file mode 100644 index 7b41eb9..0000000 --- a/app/src/main/java/com/novel/read/adapter/EditRecommendAdapter.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.constants.Constant -import com.novel.read.model.protocol.RecommendListResp -import com.novel.read.utlis.GlideImageLoader - -/** - * create by zlj on 2019/6/19 - * describe: - */ -class EditRecommendAdapter(private val mList: List) : RecyclerView.Adapter() { - - private var mContext: Context? = null - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view = LayoutInflater.from(mContext) - .inflate(R.layout.rlv_edit_recommend_item, viewGroup, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - val listBean = mList[i] - viewHolder.mTvBookName.text = listBean.book_title - viewHolder.mTvAuthor.text = listBean.author - viewHolder.mTvDescription.text = listBean.description - viewHolder.mTvHumanNum.text = listBean.getHot() - viewHolder.mTvLoveNum.text = listBean.getLike() - GlideImageLoader.displayCornerImage(mContext!!, listBean.book_cover!!, viewHolder.mIvBook) - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, listBean.book_id) - mContext!!.startActivity(intent) - } - } - - override fun getItemCount(): Int { - return mList.size - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mIvBook: ImageView = itemView.findViewById(R.id.iv_book) - var mTvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var mTvAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - var mTvDescription: TextView = itemView.findViewById(R.id.tv_book_description) - var mTvHumanNum: TextView = itemView.findViewById(R.id.tv_human_num) - var mTvLoveNum: TextView = itemView.findViewById(R.id.tv_love_look_num) - } -} diff --git a/app/src/main/java/com/novel/read/adapter/HistoryAdapter.kt b/app/src/main/java/com/novel/read/adapter/HistoryAdapter.kt deleted file mode 100644 index b3fb85f..0000000 --- a/app/src/main/java/com/novel/read/adapter/HistoryAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.model.db.SearchListTable - -/** - * create by zlj on 2019/6/17 - * describe: - */ -class HistoryAdapter(private val mList: List) : - RecyclerView.Adapter() { - private var mContext: Context? = null - - private lateinit var mClickListener: OnItemClickListener - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mTvLabel: TextView = itemView.findViewById(R.id.tv_label) - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view = LayoutInflater.from(mContext).inflate(R.layout.item_label, viewGroup, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - viewHolder.mTvLabel.text = mList[i].key - viewHolder.itemView.setOnClickListener { view -> mClickListener.onItemClick(view, i) } - } - - - override fun getItemCount(): Int { - return mList.size - } - - - fun setOnItemClickListener(mListener: OnItemClickListener) { - this.mClickListener = mListener - } - - - interface OnItemClickListener { - fun onItemClick(view: View, pos: Int) - } -} diff --git a/app/src/main/java/com/novel/read/adapter/HotAdapter.kt b/app/src/main/java/com/novel/read/adapter/HotAdapter.kt deleted file mode 100644 index 43fe3fa..0000000 --- a/app/src/main/java/com/novel/read/adapter/HotAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.constants.Constant - -/** - * create by 赵利君 on 2019/6/17 - * describe: - */ -class HotAdapter(private val mList: List) : RecyclerView.Adapter() { - private var mContext: Context? = null - private lateinit var mClickListener: OnItemClickListener - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view = LayoutInflater.from(mContext).inflate(R.layout.item_label, viewGroup, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - viewHolder.mTvLabel.text = mList[i] - viewHolder.mTvLabel.setBackgroundColor(Constant.tagColors[i]) - viewHolder.itemView.setOnClickListener { view -> mClickListener.onItemClick(view, i) } - - } - - override fun getItemCount(): Int { - return if (mList.size > 8) { - 8 - } else mList.size - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mTvLabel: TextView = itemView.findViewById(R.id.tv_label) - } - - fun setOnItemClickListener(mListener: OnItemClickListener) { - this.mClickListener = mListener - } - - - interface OnItemClickListener { - fun onItemClick(view: View, pos: Int) - } -} diff --git a/app/src/main/java/com/novel/read/adapter/HumanAdapter.kt b/app/src/main/java/com/novel/read/adapter/HumanAdapter.kt deleted file mode 100644 index 94a744a..0000000 --- a/app/src/main/java/com/novel/read/adapter/HumanAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.constants.Constant -import com.novel.read.dp2px -import com.novel.read.getScreenContentWidth -import com.novel.read.model.protocol.RecommendListResp -import com.novel.read.utlis.GlideImageLoader -import kotlinx.android.synthetic.main.rlv_human_item.view.* - -/** - * create by zlj on 2019/6/19 - * describe: - */ -class HumanAdapter(private val mList: List) : RecyclerView.Adapter() { - private lateinit var mContext: Context - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { - mContext = viewGroup.context - val view = LayoutInflater.from(mContext).inflate(R.layout.rlv_human_item, viewGroup, false) - val ivSize = (mContext.getScreenContentWidth() - mContext.dp2px(10) * 4) / 3 - view.cl_layout.layoutParams.width = ivSize - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - viewHolder.mTvBookName.text = mList[i].book_title - viewHolder.mTvAuthor.text = mList[i].author - GlideImageLoader.displayCornerImage(mContext, mList[i].book_cover!!, viewHolder.mIvBook) - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, mList[i].book_id) - mContext.startActivity(intent) - } - } - - override fun getItemCount(): Int { - return mList.size - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mIvBook: ImageView = itemView.findViewById(R.id.iv_book) - var mTvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var mTvAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - - } -} diff --git a/app/src/main/java/com/novel/read/adapter/IViewHolder.kt b/app/src/main/java/com/novel/read/adapter/IViewHolder.kt deleted file mode 100644 index b69585e..0000000 --- a/app/src/main/java/com/novel/read/adapter/IViewHolder.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.adapter - -import android.view.View -import android.view.ViewGroup - -/** - * Created by zlj - */ - -interface IViewHolder { - fun createItemView(parent: ViewGroup): View - fun initView() - fun onBind(data: T, pos: Int) - fun onClick() -} diff --git a/app/src/main/java/com/novel/read/adapter/LoveLyAdapter.kt b/app/src/main/java/com/novel/read/adapter/LoveLyAdapter.kt deleted file mode 100644 index 6999f79..0000000 --- a/app/src/main/java/com/novel/read/adapter/LoveLyAdapter.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.constants.Constant -import com.novel.read.model.protocol.RecommendBookResp -import com.novel.read.utlis.GlideImageLoader - -/** - * 猜你喜欢adapter - */ -class LoveLyAdapter(private val mList: List) : RecyclerView.Adapter() { - - private var mContext: Context? = null - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view: View = LayoutInflater.from(mContext).inflate(R.layout.rlv_item_lovely, viewGroup, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - if (viewHolder is ViewHolder) { - val bookBean = mList[i] - GlideImageLoader.displayCornerImage(mContext!!, bookBean.cover!!, viewHolder.mIvBook) - viewHolder.mTvBookName.text = bookBean.title - viewHolder.mTvBookAuthor.text = - mContext!!.getString(R.string.author_zhu, bookBean.author) - viewHolder.mTvDescription.text = bookBean.description - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, bookBean.id) - mContext!!.startActivity(intent) - } - } - } - - override fun getItemCount(): Int { - return mList.size - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mIvBook: ImageView = itemView.findViewById(R.id.iv_book) - var mTvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var mTvBookAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - var mTvDescription: TextView = itemView.findViewById(R.id.tv_book_description) - } -} diff --git a/app/src/main/java/com/novel/read/adapter/MarkAdapter.kt b/app/src/main/java/com/novel/read/adapter/MarkAdapter.kt deleted file mode 100644 index c1e176e..0000000 --- a/app/src/main/java/com/novel/read/adapter/MarkAdapter.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.CheckBox -import android.widget.CompoundButton -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.model.db.BookSignTable -import com.novel.read.model.protocol.MarkResp - -class MarkAdapter(private val mList: List) : RecyclerView.Adapter() { - - private var mContext: Context? = null - var edit: Boolean = false - set(edit) { - field = edit - notifyDataSetChanged() - } - - val selectList: String - get() { - val signs = StringBuilder() - for (i in mList.indices) { - if (mList[i].edit) { - if (signs.toString() == "") { - signs.append(mList[i].articleId) - } else { - signs.append(",").append(mList[i].articleId) - } - } - } - return signs.toString() - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view: View = LayoutInflater.from(mContext).inflate(R.layout.rlv_item_mark, viewGroup, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - if (viewHolder is ViewHolder) { - if (this.edit) { - viewHolder.mCheck.visibility = View.VISIBLE - viewHolder.mCheck.setOnCheckedChangeListener { compoundButton, b -> - mList[i].edit = b - } - } else { - viewHolder.mCheck.visibility = View.GONE - } - viewHolder.mTvMark.text = mList[i].content - viewHolder.mCheck.isChecked = mList[i].edit - } - } - - override fun getItemCount(): Int { - return mList.size - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mTvMark: TextView = itemView.findViewById(R.id.tvMarkItem) - var mCheck: CheckBox = itemView.findViewById(R.id.checkbox) - } -} diff --git a/app/src/main/java/com/novel/read/adapter/PageStyleAdapter.kt b/app/src/main/java/com/novel/read/adapter/PageStyleAdapter.kt deleted file mode 100644 index 7c63254..0000000 --- a/app/src/main/java/com/novel/read/adapter/PageStyleAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.recyclerview.widget.RecyclerView -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.widget.page.PageLoader -import com.novel.read.widget.page.PageStyle - -class PageStyleAdapter(val mList: List, private val mPageLoader: PageLoader) : - RecyclerView.Adapter() { - private var mContext: Context? = null - private var currentChecked: Int = 0 - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): PageHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view = LayoutInflater.from(mContext).inflate(R.layout.item_read_bg, viewGroup, false) - return PageHolder(view) - } - - override fun onBindViewHolder(pageHolder: PageHolder, i: Int) { - pageHolder.mReadBg.background = mList[i] - pageHolder.mIvChecked.visibility = View.GONE - if (currentChecked == i) { - pageHolder.mIvChecked.visibility = View.VISIBLE - } - pageHolder.itemView.setOnClickListener { - currentChecked = i - notifyDataSetChanged() - mPageLoader.setPageStyle(PageStyle.values()[i]) - } - } - - fun setPageStyleChecked(pageStyle: PageStyle) { - currentChecked = pageStyle.ordinal - } - - override fun getItemCount(): Int { - return mList.size - } - - class PageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val mReadBg: View = itemView.findViewById(R.id.read_bg_view) - val mIvChecked: ImageView = itemView.findViewById(R.id.read_bg_iv_checked) - - } -} diff --git a/app/src/main/java/com/novel/read/adapter/RankAdapter.kt b/app/src/main/java/com/novel/read/adapter/RankAdapter.kt deleted file mode 100644 index 17dd482..0000000 --- a/app/src/main/java/com/novel/read/adapter/RankAdapter.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.constants.Constant -import com.novel.read.dp2px -import com.novel.read.getScreenContentWidth -import com.novel.read.model.protocol.RecommendListResp -import com.novel.read.utlis.GlideImageLoader -import kotlinx.android.synthetic.main.rlv_human_item.view.* - -/** - * create by zlj on 2019/6/20 - * describe: - */ -class RankAdapter(private val mList: List) : - RecyclerView.Adapter() { - private lateinit var mContext: Context - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { - mContext = viewGroup.context - val view = LayoutInflater.from(mContext).inflate(R.layout.rlv_human_item, viewGroup, false) - val ivSize = (mContext.getScreenContentWidth() - mContext.dp2px(10) * 4) / 3 - view.cl_layout.layoutParams.width = ivSize - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - viewHolder.mTvBookName.text = mList[i].book_title - viewHolder.mTvAuthor.text = mList[i].author - GlideImageLoader.displayCornerImage(mContext, mList[i].book_cover!!, viewHolder.mIvBook) - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, mList[i].book_id) - mContext.startActivity(intent) - } - } - - override fun getItemCount(): Int { - return mList.size - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mIvBook: ImageView = itemView.findViewById(R.id.iv_book) - var mTvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var mTvAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - - } -} diff --git a/app/src/main/java/com/novel/read/adapter/RankListAdapter.kt b/app/src/main/java/com/novel/read/adapter/RankListAdapter.kt deleted file mode 100644 index eddf490..0000000 --- a/app/src/main/java/com/novel/read/adapter/RankListAdapter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.adapter.holder.EmptyHolder -import com.novel.read.adapter.holder.MoreHolder -import com.novel.read.constants.Constant -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.protocol.RankByUpdateResp -import com.novel.read.utlis.GlideImageLoader -import com.novel.read.constants.Constant.COMMENT_SIZE - -class RankListAdapter( val mList: List, recyclerView: RecyclerView) : RecyclerView.Adapter() { - - private lateinit var mContext: Context - var isLoadingMore: Boolean = false - private var lastVisibleItem: Int = 0 - private var totalItemCount: Int = 0 - private val visibleThreshold = 1 - private var mOnLoadMoreListener: OnLoadMoreListener? = null - - init { - if (recyclerView.layoutManager is LinearLayoutManager) { - val manager = recyclerView.layoutManager as LinearLayoutManager? - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - totalItemCount = manager!!.itemCount - lastVisibleItem = manager.findLastVisibleItemPosition() - if (!isLoadingMore && totalItemCount == lastVisibleItem + visibleThreshold && totalItemCount >= COMMENT_SIZE) { - if (mOnLoadMoreListener != null) { - mOnLoadMoreListener!!.onLoadMore() - } - } - } - }) - } - } - - fun setOnLoadMoreListener(listener: OnLoadMoreListener) { - this.mOnLoadMoreListener = listener - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - mContext = viewGroup.context - - val view: View - when (viewType) { - VALUE_ITEM -> { - view = LayoutInflater.from(mContext).inflate(R.layout.rlv_item_book_list, viewGroup, false) - return ViewHolder(view) - } - EMPTY_ITEM -> { - view = LayoutInflater.from(mContext).inflate(R.layout.rlv_empty_view, viewGroup, false) - return EmptyHolder(view) - } - PROCESS_ITEM -> { - view = LayoutInflater.from(mContext).inflate(R.layout.load_more_layout, viewGroup, false) - return MoreHolder(view) - } - } - throw IllegalArgumentException() - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - if (viewHolder is ViewHolder) { - val bookBean = mList[i] - viewHolder.tvBookName.text = bookBean.title - viewHolder.tvBookAuthor.text = bookBean.author - viewHolder.tvBookDescription.text = bookBean.description - GlideImageLoader.displayCornerImage(mContext, bookBean.cover, viewHolder.ivBook) - viewHolder.itemView.setOnClickListener { - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, bookBean.id) - mContext.startActivity(intent) - } - }else if (viewHolder is MoreHolder){ - viewHolder.bindModule(isLoadingMore) - } - } - - override fun getItemCount(): Int { - return if (mList.isEmpty()) { - 1 - } else mList.size + 1 - } - - override fun getItemViewType(position: Int): Int { - if (mList.isNotEmpty() && position == itemCount - 1) { - return PROCESS_ITEM - } - return when { - mList.isEmpty() -> EMPTY_ITEM - else -> VALUE_ITEM - } - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var ivBook: ImageView = itemView.findViewById(R.id.iv_book) - var tvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var tvBookAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - var tvBookDescription: TextView = itemView.findViewById(R.id.tv_book_description) - - } - - companion object { - - private const val VALUE_ITEM = 100 //正常item - - private const val EMPTY_ITEM = 101 //空白item - - private const val PROCESS_ITEM = 102 - } - -} diff --git a/app/src/main/java/com/novel/read/adapter/SearchAdapter.kt b/app/src/main/java/com/novel/read/adapter/SearchAdapter.kt deleted file mode 100644 index 3802822..0000000 --- a/app/src/main/java/com/novel/read/adapter/SearchAdapter.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.novel.read.R -import com.novel.read.activity.NovelBookDetailActivity -import com.novel.read.adapter.holder.EmptyHolder -import com.novel.read.adapter.holder.MoreHolder -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.COMMENT_SIZE -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.protocol.SearchResp -import com.novel.read.utlis.GlideImageLoader - -class SearchAdapter(private val mList: List, recyclerView: RecyclerView) : - RecyclerView.Adapter() { - - private var mContext: Context? = null - - private var book = false - private var mClickListener: OnItemClickListener? = null - - var isLoadingMore: Boolean = false - private var lastVisibleItem: Int = 0 - private var totalItemCount: Int = 0 - private val visibleThreshold = 1 - private var mOnLoadMoreListener: OnLoadMoreListener? = null - - init { - if (recyclerView.layoutManager is LinearLayoutManager) { - val manager = recyclerView.layoutManager as LinearLayoutManager? - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - totalItemCount = manager!!.itemCount - lastVisibleItem = manager.findLastVisibleItemPosition() - if (!isLoadingMore && totalItemCount == lastVisibleItem + visibleThreshold && totalItemCount >= COMMENT_SIZE) { - if (mOnLoadMoreListener != null) { - mOnLoadMoreListener!!.onLoadMore() - } - } - } - }) - - } - } - - fun setOnLoadMoreListener(listener: OnLoadMoreListener) { - this.mOnLoadMoreListener = listener - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view: View - when (i) { - VALUE_ITEM -> { - view = LayoutInflater.from(mContext) - .inflate(R.layout.rlv_item_search, viewGroup, false) - return ViewHolder(view) - } - BOOK_ITEM -> { - view = LayoutInflater.from(mContext) - .inflate(R.layout.rlv_item_book_list_search, viewGroup, false) - return BookHolder(view) - } - EMPTY_ITEM -> { - view = - LayoutInflater.from(mContext).inflate(R.layout.rlv_empty_view, viewGroup, false) - return EmptyHolder(view) - } - PROCESS_ITEM -> { - view = LayoutInflater.from(mContext) - .inflate(R.layout.load_more_layout, viewGroup, false) - return MoreHolder(view) - } - else -> throw IllegalArgumentException() - } - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - when (viewHolder) { - is ViewHolder -> { - val bookBean = mList[i] - viewHolder.mTvBookName.text = bookBean.title - viewHolder.itemView.setOnClickListener { view -> - mClickListener!!.onItemClick(view, i) - } - } - is BookHolder -> { - val bookBean = mList[i] - viewHolder.tvBookName.text = bookBean.title - viewHolder.tvBookAuthor.text = bookBean.author - viewHolder.tvBookDescription.text = bookBean.description - GlideImageLoader.displayCornerImage(mContext!!, bookBean.cover!!, viewHolder.ivBook) - viewHolder.itemView.setOnClickListener { view -> - val intent = Intent(mContext, NovelBookDetailActivity::class.java) - intent.putExtra(Constant.Bundle.BookId, bookBean.id) - mContext!!.startActivity(intent) - } - } - is MoreHolder -> viewHolder.bindModule(isLoadingMore) - } - } - - override fun getItemCount(): Int { - return if (mList.isEmpty()) { - 1 - } else mList.size + 1 - } - - override fun getItemViewType(position: Int): Int { - if (mList.isNotEmpty() && position == itemCount - 1) { - return PROCESS_ITEM - } - - return if (mList.isEmpty()) { - EMPTY_ITEM - } else { - if (book) { - BOOK_ITEM - } else { - VALUE_ITEM - } - } - } - - fun setHolderType(book: Boolean) { - this.book = book - notifyDataSetChanged() - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mTvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - - } - - internal class BookHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var ivBook: ImageView = itemView.findViewById(R.id.iv_book) - var tvBookName: TextView = itemView.findViewById(R.id.tv_book_name) - var tvBookAuthor: TextView = itemView.findViewById(R.id.tv_book_author) - var tvBookDescription: TextView = itemView.findViewById(R.id.tv_book_description) - - } - - fun setOnItemClickListener(mListener: OnItemClickListener) { - this.mClickListener = mListener - } - - interface OnItemClickListener { - fun onItemClick(view: View, pos: Int) - } - - companion object { - - private const val VALUE_ITEM = 100 //正常item - - private const val EMPTY_ITEM = 101 //空白item - - private const val BOOK_ITEM = 102 //书本item - - private const val PROCESS_ITEM = 103 - } - -} diff --git a/app/src/main/java/com/novel/read/adapter/StackAdapter.kt b/app/src/main/java/com/novel/read/adapter/StackAdapter.kt deleted file mode 100644 index a729319..0000000 --- a/app/src/main/java/com/novel/read/adapter/StackAdapter.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.novel.read.adapter - -import android.content.Context -import android.content.Intent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R -import com.novel.read.activity.NovelBookTypeListActivity -import com.novel.read.constants.Constant -import com.novel.read.model.protocol.CategoryTypeResp -import com.novel.read.utlis.GlideImageLoader - -class StackAdapter(private val mList: List) : RecyclerView.Adapter() { - - private var mContext: Context? = null - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): RecyclerView.ViewHolder { - if (mContext == null) { - mContext = viewGroup.context - } - val view: View = LayoutInflater.from(mContext).inflate(R.layout.rlv_item_book_type, viewGroup, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) { - if (viewHolder is ViewHolder) { - - var input = mList[i].title - val regex = "(.{2})" - input = input.replace(regex.toRegex(), "$1\n") - viewHolder.mTvType.text = input - viewHolder.itemView.setOnClickListener { view -> - val intent = Intent(mContext, NovelBookTypeListActivity::class.java) - intent.putExtra(Constant.Bundle.CategoryId, mList[i].id.toString()) - intent.putExtra(Constant.Bundle.mTitle, mList[i].title) - mContext!!.startActivity(intent) - } - GlideImageLoader.displayCornerImage( - mContext!!, - mList[i].cover, - viewHolder.mIvType, - R.drawable.ic_type_default - ) - } - } - - override fun getItemCount(): Int { - return mList.size - } - - internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - var mIvType: ImageView = itemView.findViewById(R.id.iv_book) - var mTvType: TextView = itemView.findViewById(R.id.tv_book_name) - - } -} diff --git a/app/src/main/java/com/novel/read/adapter/ViewHolderImpl.java b/app/src/main/java/com/novel/read/adapter/ViewHolderImpl.java deleted file mode 100644 index 5ba4f86..0000000 --- a/app/src/main/java/com/novel/read/adapter/ViewHolderImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.novel.read.adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.jetbrains.annotations.NotNull; - -public abstract class ViewHolderImpl implements IViewHolder { - private View view; - private Context context; - - protected abstract int getItemLayoutId(); - - @NotNull - @Override - public View createItemView(ViewGroup parent) { - view = LayoutInflater.from(parent.getContext()) - .inflate(getItemLayoutId(), parent, false); - context = parent.getContext(); - return view; - } - - protected V findById(int id){ - return (V) view.findViewById(id); - } - - protected Context getContext(){ - return context; - } - - protected View getItemView(){ - return view; - } - - @Override - public void onClick() { - } -} diff --git a/app/src/main/java/com/novel/read/adapter/ViewPageAdapter.kt b/app/src/main/java/com/novel/read/adapter/ViewPageAdapter.kt deleted file mode 100644 index dd4d2f7..0000000 --- a/app/src/main/java/com/novel/read/adapter/ViewPageAdapter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.novel.read.adapter - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter - -/** - * Created by Administrator on 2017/2/24. - */ - -class ViewPageAdapter(fm: FragmentManager, private val fragmentList: List) : - FragmentPagerAdapter(fm) { - - override fun getItem(position: Int): Fragment { - return fragmentList[position] - } - - override fun getCount(): Int { - return fragmentList.size - } - -} diff --git a/app/src/main/java/com/novel/read/adapter/holder/CategoryHolder.java b/app/src/main/java/com/novel/read/adapter/holder/CategoryHolder.java deleted file mode 100644 index 5dce728..0000000 --- a/app/src/main/java/com/novel/read/adapter/holder/CategoryHolder.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.novel.read.adapter.holder; - -import android.graphics.drawable.Drawable; -import android.widget.TextView; - -import androidx.core.content.ContextCompat; - -import com.novel.read.R; -import com.novel.read.adapter.ViewHolderImpl; -import com.novel.read.model.db.dbManage.BookManager; -import com.novel.read.utlis.StringUtils; -import com.novel.read.widget.page.TxtChapter; - -public class CategoryHolder extends ViewHolderImpl { - - private TextView mTvChapter; - - @Override - public void initView() { - mTvChapter = findById(R.id.category_tv_chapter); - } - - @Override - public void onBind(TxtChapter value, int pos){ - //首先判断是否该章已下载 - Drawable drawable = null; - if (value.getBookId() != null && BookManager.isChapterCached(value.getBookId(),value.getTitle())){ - drawable = ContextCompat.getDrawable(getContext(),R.drawable.selector_category_load); - } - else { - drawable = ContextCompat.getDrawable(getContext(), R.drawable.selector_category_unload); - } - - mTvChapter.setSelected(false); - mTvChapter.setTextColor(ContextCompat.getColor(getContext(),R.color.colorTitle)); - mTvChapter.setCompoundDrawablesWithIntrinsicBounds(drawable,null,null,null); - mTvChapter.setText(StringUtils.INSTANCE.convertCC(value.getTitle())); - - } - - @Override - protected int getItemLayoutId() { - return R.layout.rlv_item_category; - } - - public void setSelectedChapter(){ - mTvChapter.setTextColor(ContextCompat.getColor(getContext(),R.color.light_red)); - mTvChapter.setSelected(true); - } -} diff --git a/app/src/main/java/com/novel/read/adapter/holder/EmptyHolder.kt b/app/src/main/java/com/novel/read/adapter/holder/EmptyHolder.kt deleted file mode 100644 index 20736cf..0000000 --- a/app/src/main/java/com/novel/read/adapter/holder/EmptyHolder.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.novel.read.adapter.holder - -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class EmptyHolder(itemView: View) : RecyclerView.ViewHolder(itemView) diff --git a/app/src/main/java/com/novel/read/adapter/holder/MoreHolder.kt b/app/src/main/java/com/novel/read/adapter/holder/MoreHolder.kt deleted file mode 100644 index 2e171be..0000000 --- a/app/src/main/java/com/novel/read/adapter/holder/MoreHolder.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.novel.read.adapter.holder - -import android.view.View -import android.widget.ProgressBar -import android.widget.TextView - -import androidx.recyclerview.widget.RecyclerView - -import com.novel.read.R - -/** - * @author: LiJun 390057892@qq.com - * @date: 2018/4/4 9:28 - */ - -class MoreHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - private val mProgressBar: ProgressBar = itemView.findViewById(R.id.progressBar) - private val mTvName: TextView = itemView.findViewById(R.id.tv_name) - private val mTvEnd: TextView = itemView.findViewById(R.id.tv_end) - - fun bindModule(loadMore: Boolean) { - if (loadMore) { - mTvName.visibility = View.VISIBLE - mProgressBar.visibility = View.VISIBLE - mTvEnd.visibility = View.GONE - } else { - mTvName.visibility = View.GONE - mProgressBar.visibility = View.GONE - mTvEnd.visibility = View.VISIBLE - } - } - -} diff --git a/app/src/main/java/com/novel/read/base/BaseActivity.kt b/app/src/main/java/com/novel/read/base/BaseActivity.kt new file mode 100644 index 0000000..557a224 --- /dev/null +++ b/app/src/main/java/com/novel/read/base/BaseActivity.kt @@ -0,0 +1,174 @@ +package com.novel.read.base + +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.util.AttributeSet +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import com.novel.read.R +import com.novel.read.constant.AppConst +import com.novel.read.constant.Theme +import com.novel.read.lib.* +import com.novel.read.ui.widget.TitleBar +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.LanguageUtils +import com.novel.read.utils.ext.disableAutoFill +import com.novel.read.utils.ext.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel + +abstract class BaseActivity( + private val layoutID: Int, + val fullScreen: Boolean = true, + private val theme: Theme = Theme.Auto, + private val toolBarTheme: Theme = Theme.Auto, + private val transparent: Boolean = false +) : AppCompatActivity(), + CoroutineScope by MainScope() { + + val isInMultiWindow: Boolean + get() { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + isInMultiWindowMode + } else { + false + } + } + + override fun attachBaseContext(newBase: Context) { + super.attachBaseContext(LanguageUtils.setConfiguration(newBase)) + } + + override fun onCreateView( + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + if (AppConst.menuViewNames.contains(name) && parent?.parent is FrameLayout) { + (parent.parent as View).setBackgroundColor(backgroundColor) + } + return super.onCreateView(parent, name, context, attrs) + } + + override fun onCreate(savedInstanceState: Bundle?) { + window.decorView.disableAutoFill() + initTheme() + setupSystemBar() + super.onCreate(savedInstanceState) + setContentView(layoutID) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen) + } + onActivityCreated(savedInstanceState) + observeLiveBus() + } + + override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) { + super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) + findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen) + setupSystemBar() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(isInMultiWindow, fullScreen) + setupSystemBar() + } + + override fun onDestroy() { + super.onDestroy() + cancel() + } + + abstract fun onActivityCreated(savedInstanceState: Bundle?) + + final override fun onCreateOptionsMenu(menu: Menu?): Boolean { + return menu?.let { + val bool = onCompatCreateOptionsMenu(it) + it.applyTint(this, toolBarTheme) + bool + } ?: super.onCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.applyOpenTint(this) + return super.onMenuOpened(featureId, menu) + } + + open fun onCompatCreateOptionsMenu(menu: Menu) = super.onCreateOptionsMenu(menu) + + final override fun onOptionsItemSelected(item: MenuItem): Boolean { + item.let { + if (it.itemId == android.R.id.home) { + supportFinishAfterTransition() + return true + } + } + return onCompatOptionsItemSelected(item) + } + + open fun onCompatOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item) + + private fun initTheme() { + when (theme) { + Theme.Transparent -> setTheme(R.style.AppTheme_Transparent) + Theme.Dark -> { + setTheme(R.style.AppTheme_Dark) + ATH.applyBackgroundTint(window.decorView) + } + Theme.Light -> { + setTheme(R.style.AppTheme_Light) + ATH.applyBackgroundTint(window.decorView) + } + else -> { + if (ColorUtils.isColorLight(primaryColor)) { + setTheme(R.style.AppTheme_Light) + } else { + setTheme(R.style.AppTheme_Dark) + } + ATH.applyBackgroundTint(window.decorView) + } + } + } + + private fun setupSystemBar() { + if (fullScreen && !isInMultiWindow) { + window.clearFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + ) + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + window.decorView.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + } + ATH.setStatusBarColorAuto(this, fullScreen) + if (toolBarTheme == Theme.Dark) { + ATH.setLightStatusBar(window, false) + } else if (toolBarTheme == Theme.Light) { + ATH.setLightStatusBar(window, true) + } + upNavigationBarColor() + } + + open fun upNavigationBarColor() { + ATH.setNavigationBarColorAuto(this) + } + + open fun observeLiveBus() { + } + + override fun finish() { + currentFocus?.hideSoftInput() + super.finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/base/BaseDialogFragment.kt b/app/src/main/java/com/novel/read/base/BaseDialogFragment.kt new file mode 100644 index 0000000..8cdb006 --- /dev/null +++ b/app/src/main/java/com/novel/read/base/BaseDialogFragment.kt @@ -0,0 +1,58 @@ +package com.novel.read.base + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.novel.read.lib.ThemeStore +import com.novel.read.help.coroutine.Coroutine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + + +abstract class BaseDialogFragment : DialogFragment(), CoroutineScope { + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + private lateinit var job: Job + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + job = Job() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + view.setBackgroundColor(ThemeStore.backgroundColor()) + onFragmentCreated(view, savedInstanceState) + observeLiveBus() + } + + abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) + + override fun show(manager: FragmentManager, tag: String?) { + try { + //在每个add事务前增加一个remove事务,防止连续的add + manager.beginTransaction().remove(this).commit() + super.show(manager, tag) + } catch (e: Exception) { + //同一实例使用不同的tag会异常,这里捕获一下 + e.printStackTrace() + } + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + } + + fun execute( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ) = Coroutine.async(scope, context) { block() } + + open fun observeLiveBus() { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/base/BaseFragment.kt b/app/src/main/java/com/novel/read/base/BaseFragment.kt new file mode 100644 index 0000000..feaacf5 --- /dev/null +++ b/app/src/main/java/com/novel/read/base/BaseFragment.kt @@ -0,0 +1,96 @@ +package com.novel.read.base + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.os.Bundle +import android.view.* +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import com.novel.read.R +import com.novel.read.ui.widget.TitleBar +import com.novel.read.utils.ext.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + +@Suppress("MemberVisibilityCanBePrivate") +abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), + CoroutineScope { + lateinit var job: Job + var supportToolbar: Toolbar? = null + private set + + val menuInflater: MenuInflater + @SuppressLint("RestrictedApi") + get() = SupportMenuInflater(requireContext()) + + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + job = Job() + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + onMultiWindowModeChanged() + onFragmentCreated(view, savedInstanceState) + observeLiveBus() + } + + abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) + + override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) { + super.onMultiWindowModeChanged(isInMultiWindowMode) + onMultiWindowModeChanged() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + onMultiWindowModeChanged() + } + + private fun onMultiWindowModeChanged() { + (activity as? BaseActivity)?.let { + view?.findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(it.isInMultiWindow, it.fullScreen) + } + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + } + + fun setSupportToolbar(toolbar: Toolbar) { + supportToolbar = toolbar + supportToolbar?.let { + it.menu.apply { + onCompatCreateOptionsMenu(this) + applyTint(requireContext()) + } + + it.setOnMenuItemClickListener { item -> + onCompatOptionsItemSelected(item) + true + } + } + } + + open fun observeLiveBus() { + } + + open fun onCompatCreateOptionsMenu(menu: Menu) { + } + + open fun onCompatOptionsItemSelected(item: MenuItem) { + } + +} diff --git a/app/src/main/java/com/novel/read/base/BasePreferenceFragment.kt b/app/src/main/java/com/novel/read/base/BasePreferenceFragment.kt new file mode 100644 index 0000000..e10fb01 --- /dev/null +++ b/app/src/main/java/com/novel/read/base/BasePreferenceFragment.kt @@ -0,0 +1,60 @@ +package com.novel.read.base + +import android.annotation.SuppressLint +import androidx.fragment.app.DialogFragment +import androidx.preference.* +import com.novel.read.ui.widget.prefs.EditTextPreferenceDialog +import com.novel.read.ui.widget.prefs.ListPreferenceDialog +import com.novel.read.ui.widget.prefs.MultiSelectListPreferenceDialog + +abstract class BasePreferenceFragment : PreferenceFragmentCompat() { + + private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG" + + @SuppressLint("RestrictedApi") + override fun onDisplayPreferenceDialog(preference: Preference) { + + var handled = false + if (callbackFragment is OnPreferenceDisplayDialogCallback) { + handled = + (callbackFragment as OnPreferenceDisplayDialogCallback) + .onPreferenceDisplayDialog(this, preference) + } + if (!handled && activity is OnPreferenceDisplayDialogCallback) { + handled = (activity as OnPreferenceDisplayDialogCallback) + .onPreferenceDisplayDialog(this, preference) + } + + if (handled) { + return + } + + // check if dialog is already showing + if (parentFragmentManager.findFragmentByTag(dialogFragmentTag) != null) { + return + } + + val f: DialogFragment = when (preference) { + is EditTextPreference -> { + EditTextPreferenceDialog.newInstance(preference.getKey()) + } + is ListPreference -> { + ListPreferenceDialog.newInstance(preference.getKey()) + } + is MultiSelectListPreference -> { + MultiSelectListPreferenceDialog.newInstance(preference.getKey()) + } + else -> { + throw IllegalArgumentException( + "Cannot display dialog for an unknown Preference type: " + + preference.javaClass.simpleName + + ". Make sure to implement onPreferenceDisplayDialog() to handle " + + "displaying a custom dialog for this Preference." + ) + } + } + f.setTargetFragment(this, 0) + f.show(parentFragmentManager, dialogFragmentTag) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/base/BaseService.kt b/app/src/main/java/com/novel/read/base/BaseService.kt index 1a20fc6..d179802 100644 --- a/app/src/main/java/com/novel/read/base/BaseService.kt +++ b/app/src/main/java/com/novel/read/base/BaseService.kt @@ -1,25 +1,26 @@ package com.novel.read.base import android.app.Service +import android.content.Intent +import com.novel.read.help.coroutine.Coroutine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlin.coroutines.CoroutineContext -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable +abstract class BaseService : Service(), CoroutineScope by MainScope() { -abstract class BaseService : Service() { + fun execute( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ) = Coroutine.async(scope, context) { block() } - private var mDisposable: CompositeDisposable? = null - - protected fun addDisposable(disposable: Disposable) { - if (mDisposable == null) { - mDisposable = CompositeDisposable() - } - mDisposable!!.add(disposable) - } + override fun onBind(intent: Intent?) = null override fun onDestroy() { super.onDestroy() - if (mDisposable != null) { - mDisposable!!.dispose() - } + cancel() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/base/BaseViewModel.kt b/app/src/main/java/com/novel/read/base/BaseViewModel.kt new file mode 100644 index 0000000..d0e99f7 --- /dev/null +++ b/app/src/main/java/com/novel/read/base/BaseViewModel.kt @@ -0,0 +1,154 @@ +package com.novel.read.base + +import android.app.Application +import android.content.Context +import androidx.annotation.CallSuper +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.novel.read.App +import com.novel.read.R +import com.novel.read.network.api.ApiException +import com.novel.read.help.coroutine.Coroutine +import kotlinx.coroutines.* +import org.jetbrains.anko.AnkoLogger +import org.jetbrains.anko.toast +import retrofit2.HttpException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import kotlin.coroutines.CoroutineContext + +typealias Block = suspend (CoroutineScope) -> T +typealias Error = suspend (Exception) -> Unit +typealias Cancel = suspend (Exception) -> Unit + +open class BaseViewModel(application: Application) : AndroidViewModel(application), + CoroutineScope by MainScope(), + AnkoLogger { + + val context: Context by lazy { this.getApplication() } + + fun execute( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine.async(scope, context) { block() } + } + + fun submit( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> Deferred + ): Coroutine { + return Coroutine.async(scope, context) { block().await() } + } + + @CallSuper + override fun onCleared() { + super.onCleared() + cancel() + } + + open fun toast(message: Int) { + launch { + context.toast(message) + } + } + + open fun toast(message: CharSequence?) { + launch { + context.toast(message ?: toString()) + } + } + + open fun longToast(message: Int) { + launch { + context.toast(message) + } + } + + open fun longToast(message: CharSequence?) { + launch { + context.toast(message ?: toString()) + } + } + + /** + * 创建并执行协程 + * @param block 协程中执行 + * @param error 错误时执行 + * @param cancel 取消时只需 + * @param showErrorToast 是否弹出错误吐司 + * @return Job + */ + protected fun launch( + block: Block, + error: Error? = null, + cancel: Cancel? = null, + showErrorToast: Boolean = true + ): Job { + return viewModelScope.launch { + try { + block.invoke(this) + } catch (e: Exception) { + when (e) { + is CancellationException -> { + cancel?.invoke(e) + } + else -> { + onError(e, showErrorToast) + error?.invoke(e) + } + } + } + } + } + + /** + * 创建并执行协程 + * @param block 协程中执行 + * @return Deferred + */ + protected fun async(block: Block): Deferred { + return viewModelScope.async { block.invoke(this) } + } + + /** + * 取消协程 + * @param job 协程job + */ + protected fun cancelJob(job: Job?) { + if (job != null && job.isActive && !job.isCompleted && !job.isCancelled) { + job.cancel() + } + } + + /** + * 统一处理错误 + * @param e 异常 + * @param showErrorToast 是否显示错误吐司 + */ + private fun onError(e: Exception, showErrorToast: Boolean) { + when (e) { + is ApiException -> { + when (e.code) { + -1001 -> { + // 登录失效,清除用户信息、清除cookie/token + } + // 其他api错误 + -1 -> if (showErrorToast) toast(e.message) + // 其他错误 + else -> if (showErrorToast) toast(e.message) + } + } + // 网络请求失败 + is ConnectException, is SocketTimeoutException, is UnknownHostException, is HttpException -> + if (showErrorToast) toast(R.string.network_request_failed) + // 其他错误 + else -> + if (showErrorToast) toast(e.message ?: return) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/base/MyApp.kt b/app/src/main/java/com/novel/read/base/MyApp.kt deleted file mode 100644 index c7fd6c7..0000000 --- a/app/src/main/java/com/novel/read/base/MyApp.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.novel.read.base - -import android.app.Application - -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.util.Log - - -import androidx.appcompat.app.AppCompatDelegate - -import com.novel.read.constants.Constant -import com.novel.read.service.DownloadService -import com.novel.read.utlis.LocalManageUtil -import com.novel.read.utlis.SpUtil - - -import org.litepal.LitePal -import kotlin.properties.Delegates - -/** - * create by zlj on 2019/6/10 - */ -class MyApp : Application() { - - override fun onCreate() { - super.onCreate() - context = applicationContext - LitePal.initialize(this) - setNight() - LocalManageUtil.setApplicationLanguage(this) - startService(Intent(context, DownloadService::class.java)) - } - - private fun setNight() { - if (SpUtil.getBooleanValue(Constant.NIGHT, false)) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } - } - - - override fun attachBaseContext(base: Context) { - SpUtil.init(base) - //保存系统选择语言 - LocalManageUtil.saveSystemCurrentLanguage(base) - super.attachBaseContext(LocalManageUtil.setLocal(base)) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - //保存系统选择语言 - LocalManageUtil.onConfigurationChanged(applicationContext) - } - - companion object { - var context: Context by Delegates.notNull() - private set - } - -} diff --git a/app/src/main/java/com/novel/read/base/NovelBaseActivity.kt b/app/src/main/java/com/novel/read/base/NovelBaseActivity.kt deleted file mode 100644 index f78b3a3..0000000 --- a/app/src/main/java/com/novel/read/base/NovelBaseActivity.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.novel.read.base - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate -import com.novel.read.R -import com.novel.read.constants.Constant -import com.novel.read.utlis.LocalManageUtil -import com.novel.read.utlis.SpUtil -import com.novel.read.utlis.StatusBarUtil - -/** - * create by 赵利君 on 2019/6/10 - * describe: - */ -abstract class NovelBaseActivity : AppCompatActivity() { - - private var mNowMode: Boolean = false - - protected abstract val layoutId: Int - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - StatusBarUtil.setBarsStyle(this, R.color.colorPrimary, true) - mNowMode = SpUtil.getBooleanValue(Constant.NIGHT) - setContentView(layoutId) - initView() - initData() - - } - - private fun setTheme() { - if (SpUtil.getBooleanValue(Constant.NIGHT) != mNowMode) { - if (SpUtil.getBooleanValue(Constant.NIGHT)) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } - recreate() - } - } - - protected abstract fun initView() - - protected abstract fun initData() - - override fun onResume() { - super.onResume() - setTheme() - } - - protected fun gone(vararg views: View) { - if (views.isNotEmpty()) { - for (view in views) { - view.visibility = View.GONE - } - } - } - - protected fun visible(vararg views: View) { - if (views.isNotEmpty()) { - for (view in views) { - view.visibility = View.VISIBLE - } - } - } - - protected fun isVisible(view: View): Boolean { - return view.visibility == View.VISIBLE - } - - override fun attachBaseContext(newBase: Context) { - super.attachBaseContext(LocalManageUtil.setLocal(newBase)) - } -} diff --git a/app/src/main/java/com/novel/read/base/NovelBaseFragment.kt b/app/src/main/java/com/novel/read/base/NovelBaseFragment.kt deleted file mode 100644 index a2c3dbb..0000000 --- a/app/src/main/java/com/novel/read/base/NovelBaseFragment.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.novel.read.base - - -import android.content.Intent -import android.os.Bundle - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.LayoutRes -import androidx.fragment.app.Fragment - - -abstract class NovelBaseFragment : Fragment() { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(getLayoutId(),null) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initView() - initData() - } - - /** - * 加载布局 - */ - @LayoutRes - abstract fun getLayoutId():Int - - abstract fun initView() - - abstract fun initData() - - fun toActivity(toClsActivity: Class<*>) { - this.toActivity(toClsActivity, null as Bundle?) - } - - fun toActivity(toClsActivity: Class<*>, bundle: Bundle?) { - val intent = Intent(this.context, toClsActivity) - if (bundle != null) { - intent.putExtras(bundle) - } - - this.startActivity(intent) - } - - fun toActivityForResult(toClsActivity: Class<*>, bundle: Bundle?, requestCode: Int) { - val intent = Intent(this.context, toClsActivity) - if (bundle != null) { - intent.putExtras(bundle) - } - - startActivityForResult(intent, requestCode) - } - - -} diff --git a/app/src/main/java/com/novel/read/base/VMBaseActivity.kt b/app/src/main/java/com/novel/read/base/VMBaseActivity.kt new file mode 100644 index 0000000..a480c8e --- /dev/null +++ b/app/src/main/java/com/novel/read/base/VMBaseActivity.kt @@ -0,0 +1,15 @@ +package com.novel.read.base + +import androidx.lifecycle.ViewModel +import com.novel.read.constant.Theme + +abstract class VMBaseActivity( + layoutID: Int, + fullScreen: Boolean = true, + theme: Theme = Theme.Auto, + toolBarTheme: Theme = Theme.Auto +) : BaseActivity(layoutID, fullScreen, theme, toolBarTheme) { + + protected abstract val viewModel: VM + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/base/VMBaseFragment.kt b/app/src/main/java/com/novel/read/base/VMBaseFragment.kt new file mode 100644 index 0000000..3ecae3e --- /dev/null +++ b/app/src/main/java/com/novel/read/base/VMBaseFragment.kt @@ -0,0 +1,9 @@ +package com.novel.read.base + +import androidx.lifecycle.ViewModel + +abstract class VMBaseFragment(layoutID: Int) : BaseFragment(layoutID) { + + protected abstract val viewModel: VM + +} diff --git a/app/src/main/java/com/novel/read/constant/AppConst.kt b/app/src/main/java/com/novel/read/constant/AppConst.kt new file mode 100644 index 0000000..c7f4876 --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/AppConst.kt @@ -0,0 +1,77 @@ +package com.novel.read.constant + +import android.annotation.SuppressLint +import android.graphics.Color +import java.text.SimpleDateFormat + +@SuppressLint("SimpleDateFormat") +object AppConst { + + const val APP_TAG = "TuZi" + + const val channelIdDownload = "channel_download" + const val channelIdReadAloud = "channel_read_aloud" + const val channelIdWeb = "channel_web" + + const val UA_NAME = "User-Agent" + const val CONCEAL = "http://yijianda8.com/conceal/" + + val timeFormat: SimpleDateFormat by lazy { + SimpleDateFormat("HH:mm") + } + + val dateFormat: SimpleDateFormat by lazy { + SimpleDateFormat("yyyy/MM/dd HH:mm") + } + + val fileNameFormat: SimpleDateFormat by lazy { + SimpleDateFormat("yy-MM-dd-HH-mm-ss") + } + + val keyboardToolChars: List by lazy { + arrayListOf( + "※", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", + "$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", + "id", "class", "tag" + ) + } + + const val notificationIdRead = 1144771 + const val notificationIdAudio = 1144772 + const val notificationIdWeb = 1144773 + const val notificationIdDownload = 1144774 + + + const val refresh = 1 + const val loading = 2 + const val complete = 3 + const val error = 4 + const val loadMore = 5 + const val loadComplete = 6 + const val loadMoreFail = 7 + const val noMore = 8 + + @kotlin.jvm.JvmField + val tagColors = intArrayOf( + Color.parseColor("#90C5F0"), + Color.parseColor("#91CED5"), + Color.parseColor("#F88F55"), + Color.parseColor("#C0AFD0"), + Color.parseColor("#E78F8F"), + Color.parseColor("#67CCB7"), + Color.parseColor("#F6BC7E"), + Color.parseColor("#90C5F0"), + Color.parseColor("#91CED5") + ) + + const val home = 0 + const val man = 1 + const val woman = 2 + + const val shellName = "001" + + val menuViewNames = arrayOf( + "com.android.internal.view.menu.ListMenuItemView", + "androidx.appcompat.view.menu.ListMenuItemView" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/AppPattern.kt b/app/src/main/java/com/novel/read/constant/AppPattern.kt new file mode 100644 index 0000000..bb89765 --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/AppPattern.kt @@ -0,0 +1,16 @@ +package com.novel.read.constant + +import java.util.regex.Pattern + +object AppPattern { + val JS_PATTERN: Pattern = + Pattern.compile("([\\w\\W]*?|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) + val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") + val imgPattern: Pattern = + Pattern.compile("", Pattern.CASE_INSENSITIVE) + + val nameRegex = Regex("\\s+作\\s*者.*") + val authorRegex = Regex(".*?作\\s*?者[::]") + val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") + val splitGroupRegex = Regex("[,;,;]") +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/BookType.kt b/app/src/main/java/com/novel/read/constant/BookType.kt new file mode 100644 index 0000000..533d2f0 --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/BookType.kt @@ -0,0 +1,13 @@ +package com.novel.read.constant + +object BookType { + const val default = 0 // 0 文本 + const val audio = 1 // 1 音频 + const val local = "loc_book" + const val net = "net_book" + + + const val serial="01" + const val stop="02" + const val complete="03" +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/EventBus.kt b/app/src/main/java/com/novel/read/constant/EventBus.kt new file mode 100644 index 0000000..f9c218c --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/EventBus.kt @@ -0,0 +1,28 @@ +package com.novel.read.constant + +object EventBus { + const val MEDIA_BUTTON = "mediaButton" + const val RECREATE = "RECREATE" + const val UP_BOOK = "upBookToc" + const val UPDATE_BOOK = "UPDATE_BOOK" + const val ALOUD_STATE = "aloud_state" + const val TTS_PROGRESS = "ttsStart" + const val TTS_DS = "ttsDs" + const val BATTERY_CHANGED = "batteryChanged" + const val TIME_CHANGED = "timeChanged" + const val UP_CONFIG = "upConfig" + const val OPEN_CHAPTER = "openChapter" + const val AUDIO_SUB_TITLE = "audioSubTitle" + const val AUDIO_STATE = "audioState" + const val AUDIO_PROGRESS = "audioProgress" + const val AUDIO_SIZE = "audioSize" + const val AUDIO_SPEED = "audioSpeed" + const val SHOW_RSS = "showRss" + const val SHOW_AD = "showAd" + const val WEB_SERVICE = "webService" + const val UP_DOWNLOAD = "upDownload" + const val SAVE_CONTENT = "saveContent" + const val CHECK_SOURCE = "checkSource" + const val CHECK_SOURCE_DONE = "checkSourceDone" + const val REPLACE_RULE_SAVE = "replaceRuleSave" +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/IntentAction.kt b/app/src/main/java/com/novel/read/constant/IntentAction.kt new file mode 100644 index 0000000..b5dfb12 --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/IntentAction.kt @@ -0,0 +1,30 @@ +package com.novel.read.constant + +object IntentAction { + const val start = "start" + const val play = "play" + const val stop = "stop" + const val resume = "resume" + const val pause = "pause" + const val addTimer = "addTimer" + const val setTimer = "setTimer" + const val prevParagraph = "prevParagraph" + const val nextParagraph = "nextParagraph" + const val upTtsSpeechRate = "upTtsSpeechRate" + const val adjustProgress = "adjustProgress" + const val adjustSpeed = "adjustSpeed" + const val prev = "prev" + const val next = "next" + const val moveTo = "moveTo" + const val init = "init" + const val remove = "remove" + + + //page skip use + const val bookTypeId = "bookTypeId" + const val channelName = "channelName" + const val bookId = "bookId" + const val rankType = "rankType" + const val homeType = "homeType" + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/LayoutType.kt b/app/src/main/java/com/novel/read/constant/LayoutType.kt new file mode 100644 index 0000000..bb98fab --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/LayoutType.kt @@ -0,0 +1,23 @@ +package com.novel.read.constant + +object LayoutType { + const val HOT = 1 + const val CLICK = 2 + const val RECOMMEND= 3 + const val END = 4 + + const val BOY_HOT = 6 + const val BOY_END = 7 + const val BOY_SEARCH = 8 + + const val GIRL_HOT = 9 + const val GIRL_END = 10 + const val GIRL_SEARCH = 11 + + const val HEAD = 100 + const val OTHER = 101 + + + const val INIT_BOOK = 102 + const val SEARCH_BOOK = 103 +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/PreferKey.kt b/app/src/main/java/com/novel/read/constant/PreferKey.kt new file mode 100644 index 0000000..54319ae --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/PreferKey.kt @@ -0,0 +1,70 @@ +package com.novel.read.constant + +object PreferKey { + const val versionCode = "versionCode" + const val language = "language" + const val themeMode = "themeMode" + const val hideStatusBar = "hideStatusBar" + const val clickTurnPage = "clickTurnPage" + const val clickAllNext = "clickAllNext" + const val hideNavigationBar = "hideNavigationBar" + const val precisionSearch = "precisionSearch" + const val speakEngine = "speakEngine" + const val readAloudByPage = "readAloudByPage" + const val ttsSpeechRate = "ttsSpeechRate" + const val prevKeys = "prevKeyCodes" + const val nextKeys = "nextKeyCodes" + const val showRss = "showRss" + const val bookshelfLayout = "bookshelfLayout" + const val bookshelfSort = "bookshelfSort" + const val recordLog = "recordLog" + const val processText = "process_text" + const val cleanCache = "cleanCache" + const val saveTabPosition = "saveTabPosition" + const val fontFolder = "fontFolder" + const val backupPath = "backupUri" + const val restoreIgnore = "restoreIgnore" + const val threadCount = "threadCount" + const val webPort = "webPort" + const val keepLight = "keep_light" + const val webService = "webService" + const val webDavUrl = "web_dav_url" + const val webDavAccount = "web_dav_account" + const val webDavPassword = "web_dav_password" + const val webDavCreateDir = "webDavCreateDir" + const val webDavCacheBackup = "webDavCacheBackup" + const val changeSourceLoadToc = "changeSourceLoadToc" + const val changeSourceLoadInfo = "changeSourceLoadInfo" + const val chineseConverterType = "chineseConverterType" + const val launcherIcon = "launcherIcon" + const val textSelectAble = "selectText" + const val lastBackup = "lastBackup" + const val shareLayout = "shareLayout" + const val readStyleSelect = "readStyleSelect" + const val systemTypefaces = "system_typefaces" + const val readBodyToLh = "readBodyToLh" + const val textFullJustify = "textFullJustify" + const val textBottomJustify = "textBottomJustify" + const val autoReadSpeed = "autoReadSpeed" + const val barElevation = "barElevation" + const val transparentStatusBar = "transparentStatusBar" + const val defaultCover = "defaultCover" + const val replaceEnableDefault = "replaceEnableDefault" + const val showBrightnessView = "showBrightnessView" + const val autoClearExpired = "autoClearExpired" + + const val cPrimary = "colorPrimary" + const val cAccent = "colorAccent" + const val cBackground = "colorBackground" + const val cBBackground = "colorBottomBackground" + + const val cNPrimary = "colorPrimaryNight" + const val cNAccent = "colorAccentNight" + const val cNBackground = "colorBackgroundNight" + const val cNBBackground = "colorBottomBackgroundNight" + + const val installCount = "installCount" + const val installTime = "installTime" + const val appraiseShow = "appraiseShow" + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/Status.kt b/app/src/main/java/com/novel/read/constant/Status.kt new file mode 100644 index 0000000..ddbab5a --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/Status.kt @@ -0,0 +1,7 @@ +package com.novel.read.constant + +object Status { + const val STOP = 0 + const val PLAY = 1 + const val PAUSE = 3 +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constant/Theme.kt b/app/src/main/java/com/novel/read/constant/Theme.kt new file mode 100644 index 0000000..d9bcfc6 --- /dev/null +++ b/app/src/main/java/com/novel/read/constant/Theme.kt @@ -0,0 +1,20 @@ +package com.novel.read.constant + +import com.novel.read.help.AppConfig +import com.novel.read.utils.ColorUtils + + +enum class Theme { + Dark, Light, Auto, Transparent; + + companion object { + fun getTheme() = + if (AppConfig.isNightTheme) Dark + else Light + + fun getTheme(backgroundColor: Int) = + if (ColorUtils.isColorLight(backgroundColor)) Light + else Dark + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/constants/Constant.kt b/app/src/main/java/com/novel/read/constants/Constant.kt deleted file mode 100644 index 630e97d..0000000 --- a/app/src/main/java/com/novel/read/constants/Constant.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.novel.read.constants - -import android.graphics.Color - -import com.novel.read.utlis.FileUtils - -import java.io.File - -// ┏┓   ┏┓ -//┏┛┻━━━┛┻┓ -//┃       ┃ -//┃   ━   ┃ -//┃ ┳┛ ┗┳ ┃ -//┃       ┃ -//┃   ┻   ┃ -//┃       ┃ -//┗━┓   ┏━┛ -// ┃   ┃ 神兽保佑 -// ┃   ┃ 代码无BUG! -// ┃   ┗━━━┓ -// ┃       ┣┓ -// ┃       ┏┛ -// ┗┓┓┏━┳┓┏┛ -// ┃┫┫ ┃┫┫ -// ┗┻┛ ┗┻┛ -/** - * Created by zlj on 2019/7/27. - * desc: 常量 - */ -object Constant { - - const val NIGHT = "NIGHT" - const val Language = "Language" - const val BookSort = "BookSort" - const val Uid = "Uid" - const val Sex = "Sex" - const val Type = "Type" - const val DateType = "DateType" - const val BookGuide = "BookGuide" //图书引导是否提示过 - - const val FORMAT_BOOK_DATE = "yyyy-MM-dd HH:mm:ss" - const val FORMAT_TIME = "HH:mm" - const val COMMENT_SIZE = 10 - - const val FeedBackEmail = "390057892@qq.com" - - /** - * 百度语音合成 - */ - const val appId = "16826023" - const val appKey = "vEuU5gIWGwq5hivdTAaKz0P9" - const val secretKey = "FcWRYUIrOPyE7dy51qfYZmg8Y1ZyP1c4 " - - //BookCachePath (因为getCachePath引用了Context,所以必须是静态变量,不能够是静态常量) - @kotlin.jvm.JvmField - var BOOK_CACHE_PATH: String = (FileUtils.getCachePath() + File.separator - + "book_cache" + File.separator) - - @kotlin.jvm.JvmField - val tagColors = intArrayOf( - Color.parseColor("#90C5F0"), - Color.parseColor("#91CED5"), - Color.parseColor("#F88F55"), - Color.parseColor("#C0AFD0"), - Color.parseColor("#E78F8F"), - Color.parseColor("#67CCB7"), - Color.parseColor("#F6BC7E"), - Color.parseColor("#90C5F0"), - Color.parseColor("#91CED5") - ) - - //榜单类型 - interface ListType { - companion object { - const val Human = "1" - const val EditRecommend = "2" - const val HotSearch = "3" - } - } - - interface GenderType { - companion object { - const val Man = "1" - const val Woman = "2" - } - } - - interface DateTyp { - companion object { - const val General = "3" - const val Month = "2" - const val Week = "1" - } - } - - interface Bundle { - companion object { - const val CategoryId = "category_id" - const val mTitle = "mTitle" - const val BookId = "BookId" - } - } - - interface RequestCode { - companion object { - const val REQUEST_READ = 1 - } - } - - interface ResultCode { - companion object { - const val RESULT_IS_COLLECTED = "result_is_collected" - } - } -} diff --git a/app/src/main/java/com/novel/read/data/README.md b/app/src/main/java/com/novel/read/data/README.md new file mode 100644 index 0000000..a935461 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/README.md @@ -0,0 +1,17 @@ +# 存储数据用 +* dao 数据操作 +* entities 数据模型 +* \Book 书籍信息 +* \BookChapter 目录信息 +* \BookGroup 书籍分组 +* \Bookmark 书签 +* \BookSource 书源 +* \Cookie http cookie +* \ReplaceRule 替换规则 +* \RssArticle rss条目 +* \RssReadRecord rss阅读记录 +* \RssSource rss源 +* \RssStar rss收藏 +* \SearchBook 搜索结果 +* \SearchKeyword 搜索关键字 +* \TxtTocRule txt文件目录规则 diff --git a/app/src/main/java/com/novel/read/data/db/BookDao.kt b/app/src/main/java/com/novel/read/data/db/BookDao.kt new file mode 100644 index 0000000..00b1896 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/BookDao.kt @@ -0,0 +1,50 @@ +package com.novel.read.data.db + +import android.content.ContentValues +import com.novel.read.data.db.entity.Book +import org.litepal.LitePal + + +class BookDao { + + fun getAllBooks(): MutableList = LitePal.findAll(Book::class.java) + + fun lastReadBook(): Book = LitePal.order("durChapterTime desc").findFirst(Book::class.java) + + fun update(book: Book) { + val values = ContentValues() + values.put("authorPenname", book.authorPenname) + values.put("bookName", book.bookName) + values.put("bookStatus", book.bookStatus) + values.put("categoryName", book.categoryName) + values.put("channelName", book.channelName) + values.put("cName", book.cName) + values.put("coverImageUrl", book.coverImageUrl) + values.put("introduction", book.introduction) + values.put("keyWord", book.keyWord) + values.put("lastUpdateChapterDate", book.lastUpdateChapterDate) + values.put("status", book.status) + values.put("wordCount", book.wordCount) + values.put("durChapterTime", book.durChapterTime) + values.put("durChapterIndex", book.durChapterIndex) + values.put("durChapterPos", book.durChapterPos) + values.put("durChapterTitle", book.durChapterTitle) + values.put("totalChapterNum", book.totalChapterNum) + LitePal.updateAll(Book::class.java, values, "bookId=?", book.bookId.toString()) + } + + fun getBook(bookId: String): Book? = + LitePal.where("bookId=?", bookId).findFirst(Book::class.java) + + fun insert(book: Book) = book.saveOrUpdate("bookId=?", book.bookId.toString()) + + fun delete(book: Book): Int = + LitePal.deleteAll(Book::class.java, "bookId=?", book.bookId.toString()) + + fun saveBook(book: List?) { + if (book != null && book.isNotEmpty()) { + LitePal.saveAll(book) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/BookDatabase.kt b/app/src/main/java/com/novel/read/data/db/BookDatabase.kt new file mode 100644 index 0000000..2b0b795 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/BookDatabase.kt @@ -0,0 +1,67 @@ +package com.novel.read.data.db + +class BookDatabase private constructor() { + + companion object { + private var instance: BookDatabase? = null + get() { + if (field == null) { + field = BookDatabase() + } + return field + } + + fun get(): BookDatabase { + return instance!! + } + } + + private var chapterDao: ChapterDao? = null + private var bookDao: BookDao? = null + private var readRecordDao: ReadRecordDao? = null + private var bookMarkDao: BookMarkDao? = null + private var searchHistoryDao: SearchHistoryDao? = null + private var userDao: UserDao? = null + + fun getChapterDao(): ChapterDao { + if (chapterDao == null) { + chapterDao = ChapterDao() + } + return chapterDao!! + } + + fun getBookDao(): BookDao { + if (bookDao == null) { + bookDao = BookDao() + } + return bookDao!! + } + + fun getReadRecordDao(): ReadRecordDao { + if (readRecordDao == null) { + readRecordDao = ReadRecordDao() + } + return readRecordDao!! + } + + fun getBookMarkDao(): BookMarkDao { + if (bookMarkDao == null) { + bookMarkDao = BookMarkDao() + } + return bookMarkDao!! + } + + fun getSearchDao(): SearchHistoryDao { + if (searchHistoryDao == null) { + searchHistoryDao = SearchHistoryDao() + } + return searchHistoryDao!! + } + + fun getUserDao(): UserDao { + if (userDao == null) { + userDao = UserDao() + } + return userDao!! + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/BookMarkDao.kt b/app/src/main/java/com/novel/read/data/db/BookMarkDao.kt new file mode 100644 index 0000000..e486936 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/BookMarkDao.kt @@ -0,0 +1,39 @@ +package com.novel.read.data.db + +import com.novel.read.data.db.entity.Bookmark +import org.litepal.LitePal + +class BookMarkDao { + + fun observeByBook(bookId: Long): List? = + LitePal.where("bookId=?", bookId.toString()).find(Bookmark::class.java) + + fun liveDataSearch(bookId: String, key: String): List? = + LitePal.where( + "bookId=? and (chapterName like ? or content like ?)", + bookId.toString(), + "%${key}%", "%${key}%" + ) + .find(Bookmark::class.java) + + fun insert(bookmark: Bookmark) { + bookmark.saveOrUpdate( + "bookId=? and chapterIndex=?", + bookmark.bookId.toString(), + bookmark.chapterIndex.toString() + ) + } + + fun update(bookmark: Bookmark) { + bookmark.saveOrUpdate( + "bookId=? and chapterIndex=?", + bookmark.bookId.toString(), + bookmark.chapterIndex.toString() + ) + } + + fun delete(bookmark: Bookmark) { + bookmark.delete() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/ChapterDao.kt b/app/src/main/java/com/novel/read/data/db/ChapterDao.kt new file mode 100644 index 0000000..399cfa0 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/ChapterDao.kt @@ -0,0 +1,48 @@ +package com.novel.read.data.db + +import com.novel.read.data.db.entity.BookChapter +import org.litepal.LitePal + +class ChapterDao { + + fun observeByBook(bookId: String): List? = + LitePal.where("bookId=?", bookId.toString()).find(BookChapter::class.java) + + fun liveDataSearch(bookId: String, key: String): List? = + LitePal.where("bookId=? and chapterName like ?", bookId, "%${key}%") + .find(BookChapter::class.java) + + fun getChapter(chapterId: Long): BookChapter? = + LitePal.where("chapterId =?", chapterId.toString()).findFirst(BookChapter::class.java) + + fun getChapter(bookId: Long, chapterIndex: Int): BookChapter? = + LitePal.where("bookId=? and chapterIndex =?", bookId.toString(), chapterIndex.toString()) + .findFirst(BookChapter::class.java) + + fun getChapterList(bookId: Long): List? = + LitePal.where("bookId=?", bookId.toString()).find(BookChapter::class.java) + + fun getChapterList(bookId: Long, start: Int, end: Int): List = + LitePal.where( + "bookId=? and chapterIndex >=? and chapterIndex <=?", + bookId.toString(), + start.toString(), + end.toString() + ).order("chapterIndex desc").find(BookChapter::class.java) + + fun getChapterCount(bookId: Long): Int { + return LitePal.where("bookId=?", bookId.toString()).count(BookChapter::class.java) + } + + fun insert(bookChapters: Array) { + if (bookChapters.isNotEmpty()) { + LitePal.deleteAll( + BookChapter::class.java, + "bookId=?", + bookChapters[0].bookId.toString() + ) + } + LitePal.saveAll(bookChapters.toList()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/ReadRecordDao.kt b/app/src/main/java/com/novel/read/data/db/ReadRecordDao.kt new file mode 100644 index 0000000..65906e8 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/ReadRecordDao.kt @@ -0,0 +1,23 @@ +package com.novel.read.data.db + +import com.novel.read.data.db.entity.ReadRecord +import org.litepal.LitePal + +class ReadRecordDao { + + fun getReadTime(bookName: String): Long? = + LitePal.where("bookName =?", bookName).findFirst(ReadRecord::class.java)?.readTime + + fun insert(readRecord: ReadRecord) { + readRecord.saveOrUpdate("bookId=?", readRecord.bookId.toString()) + } + + fun delete(readRecord: ReadRecord) { + readRecord.delete() + } + + fun getAll(): List { + return LitePal.findAll(ReadRecord::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/SearchHistoryDao.kt b/app/src/main/java/com/novel/read/data/db/SearchHistoryDao.kt new file mode 100644 index 0000000..b2955cb --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/SearchHistoryDao.kt @@ -0,0 +1,19 @@ +package com.novel.read.data.db + +import com.novel.read.data.db.entity.SearchHistory +import org.litepal.LitePal + +class SearchHistoryDao { + + fun getListByTime(): MutableList? = + LitePal.order("saveTime desc").limit(5).find(SearchHistory::class.java) + + fun insert(searchHistory: SearchHistory, key: String) { + searchHistory.saveOrUpdate("key=?", key) + } + + fun deleteAll() { + LitePal.deleteAll(SearchHistory::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/UserDao.kt b/app/src/main/java/com/novel/read/data/db/UserDao.kt new file mode 100644 index 0000000..fc66fd9 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/UserDao.kt @@ -0,0 +1,16 @@ +package com.novel.read.data.db + +import com.novel.read.data.db.entity.User +import org.litepal.LitePal +import org.litepal.extension.deleteAll + +class UserDao { + + fun saveUser(user: User) = user.saveOrUpdate("userId=?", user.userId.toString()) + + fun getUser(): User? = + LitePal.findFirst(User::class.java) + + fun deleteUser() = LitePal.deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/Book.kt b/app/src/main/java/com/novel/read/data/db/entity/Book.kt new file mode 100644 index 0000000..f7c91a8 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/Book.kt @@ -0,0 +1,52 @@ +package com.novel.read.data.db.entity + +import com.google.gson.annotations.SerializedName +import com.novel.read.constant.AppPattern +import com.novel.read.constant.BookType +import com.novel.read.utils.MD5Utils +import org.litepal.crud.LitePalSupport +import kotlin.math.max + +data class Book( + val authorPenname: String, + val bookId: Long, + val bookName: String, + val bookStatus: String, + val categoryName: String?, + val channelName: String?, + @SerializedName("className") + val cName: String?, + val coverImageUrl: String, + val introduction: String, + val keyWord: String, + var lastUpdateChapterDate: String, + val status: Int, + val wordCount: Long, + var totalChapterNum: Int = 0, // 书籍目录总数 + var durChapterTitle: String? = null, // 当前章节名称 + var durChapterIndex: Int = 0, // 当前章节索引 + var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置) + var durChapterTime: Long = System.currentTimeMillis(), // 最近一次阅读书籍的时间(打开正文的时间) + var origin: String = BookType.net, + var originName: String = "", + var bookTypeId: Int = 0 +) : LitePalSupport() { + + fun isLocalBook(): Boolean { + return origin == BookType.local + } + + fun getFolderName(): String { + return bookName.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(coverImageUrl) + } + + fun isEpub(): Boolean { + return originName.endsWith(".epub", true) + } + + fun canUpdate(): Boolean { + return bookStatus == BookType.serial + } + + fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/BookChapter.kt b/app/src/main/java/com/novel/read/data/db/entity/BookChapter.kt new file mode 100644 index 0000000..3052fd0 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/BookChapter.kt @@ -0,0 +1,19 @@ +package com.novel.read.data.db.entity + +import org.litepal.crud.LitePalSupport + +data class BookChapter( +// val bookId: Int, +// val chapterList: List, +// val volumeId: Int, +// val volumeName: String + + val chapterId: Long, + val bookId: Long, + val chapterIndex: Int, + var chapterName: String, + val createTimeValue: Long, + val updateDate: String, + val updateTimeValue: Long, + val chapterUrl: String?, +) : LitePalSupport() \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/Bookmark.kt b/app/src/main/java/com/novel/read/data/db/entity/Bookmark.kt new file mode 100644 index 0000000..0132bdf --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/Bookmark.kt @@ -0,0 +1,16 @@ +package com.novel.read.data.db.entity + +import org.litepal.crud.LitePalSupport +import java.io.Serializable + +data class Bookmark( + var time: Long = System.currentTimeMillis(), + var bookId: Long = 0, + var bookName: String = "", + val bookAuthor: String = "", + var chapterIndex: Int = 0, + var pageIndex: Int = 0, + var chapterName: String = "", + var content: String = "" + +) : LitePalSupport(), Serializable \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/ChapterDetailEntity.kt b/app/src/main/java/com/novel/read/data/db/entity/ChapterDetailEntity.kt new file mode 100644 index 0000000..f3942ab --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/ChapterDetailEntity.kt @@ -0,0 +1,10 @@ +package com.novel.read.data.db.entity + +data class ChapterDetailEntity( + val chapterContent: String, + val chapterIndex: Int, + val chapterName: String, + val isRecommend: String, + val isVIP: String, + val wordCount: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/HttpTTS.kt b/app/src/main/java/com/novel/read/data/db/entity/HttpTTS.kt new file mode 100644 index 0000000..0d3d8fa --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/HttpTTS.kt @@ -0,0 +1,9 @@ +package com.novel.read.data.db.entity + +import org.litepal.crud.LitePalSupport + +data class HttpTTS( + val id: Long = System.currentTimeMillis(), + var name: String = "", + var url: String = "" +) : LitePalSupport() \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/ReadRecord.kt b/app/src/main/java/com/novel/read/data/db/entity/ReadRecord.kt new file mode 100644 index 0000000..36f806c --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/ReadRecord.kt @@ -0,0 +1,15 @@ +package com.novel.read.data.db.entity + +import org.litepal.crud.LitePalSupport + +import java.io.Serializable + +/** + * Created by zlj + */ +class ReadRecord( + var androidId: String = "", + var bookName: String = "", + var bookId: Long = 0L, + var readTime: Long = 0L +) : LitePalSupport(), Serializable diff --git a/app/src/main/java/com/novel/read/model/db/SearchListTable.kt b/app/src/main/java/com/novel/read/data/db/entity/SearchHistory.kt similarity index 63% rename from app/src/main/java/com/novel/read/model/db/SearchListTable.kt rename to app/src/main/java/com/novel/read/data/db/entity/SearchHistory.kt index 6d6053a..2b42e37 100644 --- a/app/src/main/java/com/novel/read/model/db/SearchListTable.kt +++ b/app/src/main/java/com/novel/read/data/db/entity/SearchHistory.kt @@ -1,20 +1,13 @@ -package com.novel.read.model.db - +package com.novel.read.data.db.entity +import com.novel.read.utils.StringUtils import org.litepal.crud.LitePalSupport - import java.io.Serializable -/** - * create by zlj on 2019/6/19 - * describe: - */ -class SearchListTable : LitePalSupport(), Serializable { - - var key: String = "" +class SearchHistory( + var key: String = "", var saveTime: Long = 0 - - +) : LitePalSupport(), Serializable { override fun equals(other: Any?): Boolean { return if (other != null && other.toString() == key) { true @@ -26,4 +19,8 @@ class SearchListTable : LitePalSupport(), Serializable { result = 31 * result + saveTime.hashCode() return result } -} + + fun getBKey(): String { + return StringUtils.convertCC(key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/db/entity/User.kt b/app/src/main/java/com/novel/read/data/db/entity/User.kt new file mode 100644 index 0000000..cb63dd1 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/db/entity/User.kt @@ -0,0 +1,29 @@ +package com.novel.read.data.db.entity + +import org.litepal.crud.LitePalSupport +import java.io.Serializable + +data class User( + val createTime: Long, + val email: String, + val headImageUrl: String, + val idToken: String, + val introduction: String?, + val ip: String, + val isEmailVerified: Int, + val lastLoginTime: Long?, + val nickName: String?, + val oldNickName: String?, + val os: String, + val password: String, + val phone: String, + val provider: String, + val sex: Int, + val updateTime: Long?, + val userId: Long, + val userName: String, + val validFlag: Int, + val vipEndTime: Long?, + val vipStartTime: Long?, + val vipStatus: Int +) : LitePalSupport(), Serializable \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/AppUpdateResp.kt b/app/src/main/java/com/novel/read/data/model/AppUpdateResp.kt new file mode 100644 index 0000000..23a9a6b --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/AppUpdateResp.kt @@ -0,0 +1,20 @@ +package com.novel.read.data.model + +data class AppUpdateResp( + val appEdition: AppEdition +) + +data class AppEdition( + val createTime: String, + val editionCode: String, + val editionId: Int, + val fileUrl: String, + val forceUpdate: Int, + val insideEditionCode: Int, + val isDel: Int, + val packageSize: String, + val pushTime: String, + val shellName: String, + val status: Int, + val upgradeContent: String +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/BookInfoResp.kt b/app/src/main/java/com/novel/read/data/model/BookInfoResp.kt new file mode 100644 index 0000000..e395fc6 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/BookInfoResp.kt @@ -0,0 +1,33 @@ +package com.novel.read.data.model + +data class BookInfoResp( + val book: BookResp +) + +data class BookResp( + val authorInformation: Any, + val authorName: String, + val authorUserId: Long, + val bookClass: Any, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Any, + val bookTypeId: Int, + val categoryName: String?, + val channelName: Any?, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Any, + val hits: Any, + val hotStatus: Any, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: Any, + val rankType: Any, + val recommendStatus: Any, + val validFlag: Int, + val wordCount: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/BookListResp.kt b/app/src/main/java/com/novel/read/data/model/BookListResp.kt new file mode 100644 index 0000000..6029d23 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/BookListResp.kt @@ -0,0 +1,52 @@ +package com.novel.read.data.model + +import com.novel.read.utils.StringUtils + +data class BookListResp( + val authorInformation: String, + var authorName: String?, + val authorUserId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Int, + val categoryName: String?, + val channelName: String?, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val hotStatus: Int, + val introduction: String?, + val keyWord: String, + val lastUpdateChapterDate: String?, + val recommendStatus: Int, + val validFlag: Int, + val wordCount: Long + +) { + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName!!) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName ?: "") + } + + fun getBChannel(): String { + return StringUtils.convertCC(channelName!!) + } + + fun getBIntroduction(): String { + return StringUtils.convertCC(introduction!!) + } + + fun getBKeyWord(): String { + return StringUtils.convertCC(keyWord) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/BoyEndRank.kt b/app/src/main/java/com/novel/read/data/model/BoyEndRank.kt new file mode 100644 index 0000000..dd5f071 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/BoyEndRank.kt @@ -0,0 +1,51 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class BoyEndRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Long, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int, + val wordCount: Long +){ + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class BoyEndRankEntity(val boyEndRanks:List):MultiItemEntity{ + override val itemType: Int + get() = LayoutType.BOY_END +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/BoyHotRank.kt b/app/src/main/java/com/novel/read/data/model/BoyHotRank.kt new file mode 100644 index 0000000..1a51daf --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/BoyHotRank.kt @@ -0,0 +1,50 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class BoyHotRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Long, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +) { + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class BoyHotRankEntity(val boyHotRanks: List) : MultiItemEntity { + override val itemType: Int + get() = LayoutType.BOY_HOT +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/BoySearchRank.kt b/app/src/main/java/com/novel/read/data/model/BoySearchRank.kt new file mode 100644 index 0000000..c68c2b2 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/BoySearchRank.kt @@ -0,0 +1,50 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class BoySearchRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Long, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +){ + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class BoySearchRankEntity(val boySearchRanks:List):MultiItemEntity{ + override val itemType: Int + get() = LayoutType.BOY_SEARCH +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/ChannelResp.kt b/app/src/main/java/com/novel/read/data/model/ChannelResp.kt new file mode 100644 index 0000000..bbc722f --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/ChannelResp.kt @@ -0,0 +1,24 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.SectionEntity +import com.novel.read.utils.StringUtils + +data class ChannelResp( + val allType: List +) + +data class AllType( + val bookTypeId: Long, + val categoryName: String, + val createTime: Long, + val validFlag: Int, + val typeImageUrl:String, + val bookCount :Int +) : SectionEntity { + override val isHeader: Boolean + get() = false + + fun getBChannel(): String { + return StringUtils.convertCC(categoryName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/ChannelSection.kt b/app/src/main/java/com/novel/read/data/model/ChannelSection.kt new file mode 100644 index 0000000..2bc8941 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/ChannelSection.kt @@ -0,0 +1,8 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.SectionEntity + +data class ChannelSection( + override val isHeader: Boolean, + val obj: Any +) : SectionEntity \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/ChapterContentResp.kt b/app/src/main/java/com/novel/read/data/model/ChapterContentResp.kt new file mode 100644 index 0000000..bde4345 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/ChapterContentResp.kt @@ -0,0 +1,16 @@ +package com.novel.read.data.model + +data class ChapterContentResp( + val chapter: ChapterContent +) + +data class ChapterContent( + val bookId: Long, + val chapterContent: String, + val chapterId: Long, + val chapterIndex: Int, + val chapterName: String, + val chapterUrl: Any, + val createTime: Long, + val validFlag: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/ChapterResp.kt b/app/src/main/java/com/novel/read/data/model/ChapterResp.kt new file mode 100644 index 0000000..be34a63 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/ChapterResp.kt @@ -0,0 +1,16 @@ +package com.novel.read.data.model + +data class ChapterResp( + val chapterList: List?, + val count: Int +) + +data class Chapter( + val bookId: Long, + val chapterId: Long, + val chapterIndex: Int, + val chapterName: String, + val chapterUrl: String?, + val createTime: Long, + val validFlag: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/ClickRank.kt b/app/src/main/java/com/novel/read/data/model/ClickRank.kt new file mode 100644 index 0000000..865a281 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/ClickRank.kt @@ -0,0 +1,51 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class ClickRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Int, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int, + val wordCount: Long +){ + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class ClickEntity(val clickRanks:List):MultiItemEntity{ + override val itemType: Int + get() = LayoutType.CLICK +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/Default.kt b/app/src/main/java/com/novel/read/data/model/Default.kt new file mode 100644 index 0000000..a9450d9 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/Default.kt @@ -0,0 +1,3 @@ +package com.novel.read.data.model + +class Default() \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/EndRank.kt b/app/src/main/java/com/novel/read/data/model/EndRank.kt new file mode 100644 index 0000000..4cbc584 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/EndRank.kt @@ -0,0 +1,49 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class EndRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Int, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +){ + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class EndEntity(val endRanks: List) : MultiItemEntity { + override val itemType: Int + get() = LayoutType.END +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/GirlEndRank.kt b/app/src/main/java/com/novel/read/data/model/GirlEndRank.kt new file mode 100644 index 0000000..27b33cd --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/GirlEndRank.kt @@ -0,0 +1,51 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class GirlEndRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Long, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int, + val wordCount: Long +){ + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class GirlEndRankEntity(val girlEndRanks:List):MultiItemEntity{ + override val itemType: Int + get() = LayoutType.GIRL_END +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/GirlHotRank.kt b/app/src/main/java/com/novel/read/data/model/GirlHotRank.kt new file mode 100644 index 0000000..5c2e054 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/GirlHotRank.kt @@ -0,0 +1,51 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class GirlHotRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Long, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +){ + + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class GirlHotRankEntity(val girlHotRanks:List):MultiItemEntity{ + override val itemType: Int + get() = LayoutType.GIRL_HOT +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/GirlSearchRank.kt b/app/src/main/java/com/novel/read/data/model/GirlSearchRank.kt new file mode 100644 index 0000000..14f52ee --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/GirlSearchRank.kt @@ -0,0 +1,51 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class GirlSearchRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Long, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +){ + + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } +} + +data class GirlSearchRankEntity(val girlSearchRanks:List):MultiItemEntity{ + override val itemType: Int + get() = LayoutType.GIRL_SEARCH +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/GoodsResp.kt b/app/src/main/java/com/novel/read/data/model/GoodsResp.kt new file mode 100644 index 0000000..49e42e5 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/GoodsResp.kt @@ -0,0 +1,15 @@ +package com.novel.read.data.model + +data class GoodsResp( + val bookCommodityClass: Int, + val bookCommodityId: Long, + val commodityCode: String, + val commodityCurrentValuation: Int, + val commodityName: String, + val commodityValuation: Int, + val createTime: Long, + val discount: Int, + val remark: String, + val thirdCommodityNumber: String, + val validFlag: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/HomeBanner.kt b/app/src/main/java/com/novel/read/data/model/HomeBanner.kt new file mode 100644 index 0000000..c528876 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/HomeBanner.kt @@ -0,0 +1,11 @@ +package com.novel.read.data.model + +data class HomeBanner( + val bannerId: Int, + val bookChannelId: Long, + val bookId: Int, + val createTime: String, + val imgUrl: String, + val isDel: Int, + val type: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/HomeResp.kt b/app/src/main/java/com/novel/read/data/model/HomeResp.kt new file mode 100644 index 0000000..8adcf6a --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/HomeResp.kt @@ -0,0 +1,16 @@ +package com.novel.read.data.model + +data class HomeResp( + val wordNumRank: List, + val clickRank: List, + val starRank: List, + val recommendRank: List, + + val boyHotRank: List, + val boyEndRank: List, + val boyHotSearchRank: List, + + val girlHotRank: List, + val girlEndRank: List, + val girlHotSearchRank: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/HotRank.kt b/app/src/main/java/com/novel/read/data/model/HotRank.kt new file mode 100644 index 0000000..3942806 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/HotRank.kt @@ -0,0 +1,45 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class HotRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Int, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +){ + fun getBName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBAuthor(): String { + return StringUtils.convertCC(authorName) + } +} + +data class HotEntity(val hotRanks: List) : MultiItemEntity { + override val itemType: Int + get() = LayoutType.HOT +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/LoginReq.kt b/app/src/main/java/com/novel/read/data/model/LoginReq.kt new file mode 100644 index 0000000..77b2331 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/LoginReq.kt @@ -0,0 +1,21 @@ +package com.novel.read.data.model + +data class LoginReq( + val email: String, + val headImageUrl: String, + val idToken: String, + val ip: String, + var isEmailVerified: Int, + val nickName: String, + val os: String = "Android", + val phone: String, + val provider: String +){ + fun setIsEmail(boolean: Boolean){ + isEmailVerified = if (boolean){ + 0 + }else{ + 1 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/Pagination.kt b/app/src/main/java/com/novel/read/data/model/Pagination.kt new file mode 100644 index 0000000..da2db28 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/Pagination.kt @@ -0,0 +1,14 @@ +package com.novel.read.data.model + +import androidx.annotation.Keep + +/** + * Created by xiaojianjun on 2019-11-07. + */ +@Keep +data class Pagination( + val count: Int, + val bookList: MutableList, + val bookRankList: MutableList, + val searchTermsList: MutableList +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/PaginationSimilar.kt b/app/src/main/java/com/novel/read/data/model/PaginationSimilar.kt new file mode 100644 index 0000000..55f6eb0 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/PaginationSimilar.kt @@ -0,0 +1,9 @@ +package com.novel.read.data.model + +import androidx.annotation.Keep + + +@Keep +data class PaginationSimilar( + val recommendBookList: MutableList +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/RecommendRank.kt b/app/src/main/java/com/novel/read/data/model/RecommendRank.kt new file mode 100644 index 0000000..ca20870 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/RecommendRank.kt @@ -0,0 +1,53 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType +import com.novel.read.utils.StringUtils + +data class RecommendRank( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Int, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hits: String, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val rankNumber: String, + val rankType: String, + val recommendStatus: Int, + val validFlag: Int +) : MultiItemEntity { + override val itemType: Int + get() = LayoutType.RECOMMEND + + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } + +} + +data class RecommendEntity(val recommendRanks: List) : MultiItemEntity { + override val itemType: Int + get() = LayoutType.RECOMMEND +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/SearchResp.kt b/app/src/main/java/com/novel/read/data/model/SearchResp.kt new file mode 100644 index 0000000..0f9c4ad --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/SearchResp.kt @@ -0,0 +1,53 @@ +package com.novel.read.data.model + +import com.novel.read.utils.StringUtils + +data class SearchResp( + val authorInformation: String, + val authorName: String, + val authorUserId: Long, + val bookChannelId: Long, + val bookClass: Int, + val bookId: Long, + val bookName: String, + val bookSId: String, + val bookStatus: Int, + val bookTypeId: Int, + val categoryName: String, + val channelName: String, + val coverImageUrl: String, + val createTime: Long, + val endStatus: Int, + val extBookId: Int, + val hotStatus: Int, + val introduction: String, + val keyWord: String, + val lastUpdateChapterDate: String, + val recommendStatus: Int, + val validFlag: Int, + val wordCount: Long, +){ + fun getBAuthorName(): String { + return StringUtils.convertCC(authorName) + } + + fun getBBookName(): String { + return StringUtils.convertCC(bookName) + } + + fun getBCategoryName(): String { + return StringUtils.convertCC(categoryName) + } + + fun getBChannel(): String { + return StringUtils.convertCC(channelName) + } + + fun getBIntroduction(): String { + return StringUtils.convertCC(introduction) + } + + fun getBKeyWord(): String { + return StringUtils.convertCC(keyWord) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/Tools.kt b/app/src/main/java/com/novel/read/data/model/Tools.kt new file mode 100644 index 0000000..aecd2d5 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/Tools.kt @@ -0,0 +1,5 @@ +package com.novel.read.data.model + +class Tools(var title: String, var img: Int) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/TypeEntity.kt b/app/src/main/java/com/novel/read/data/model/TypeEntity.kt new file mode 100644 index 0000000..2a6c108 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/TypeEntity.kt @@ -0,0 +1,9 @@ +package com.novel.read.data.model + +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.constant.LayoutType + +class TypeEntity: MultiItemEntity { + override val itemType: Int + get() = LayoutType.OTHER +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/TypeName.kt b/app/src/main/java/com/novel/read/data/model/TypeName.kt new file mode 100644 index 0000000..4adbf20 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/TypeName.kt @@ -0,0 +1,69 @@ +package com.novel.read.data.model + +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.LayoutType +import kotlin.collections.ArrayList + + +data class TypeName( + val rankType: Int, + val name: String, + var check: Boolean +) { + + +} + +fun rankList(layoutType: Int): List { + val list: MutableList = + ArrayList() + list.clear() + list.add(TypeName(LayoutType.HOT, App.INSTANCE.getString(R.string.big_hot_title), false)) + list.add(TypeName(LayoutType.CLICK, App.INSTANCE.getString(R.string.click_title), false)) + list.add( + TypeName( + LayoutType.RECOMMEND, + App.INSTANCE.getString(R.string.recommend_title), + false + ) + ) + list.add(TypeName(LayoutType.END, App.INSTANCE.getString(R.string.end_title), false)) + + list[layoutType - 1].check = true + return list +} + +fun manList(layoutType: Int): List { + val list: MutableList = + ArrayList() + list.clear() + list.add(TypeName(LayoutType.BOY_HOT, App.INSTANCE.getString(R.string.big_hot_title), false)) + list.add(TypeName(LayoutType.BOY_END, App.INSTANCE.getString(R.string.end_title2), false)) + list.add( + TypeName( + LayoutType.BOY_SEARCH, + App.INSTANCE.getString(R.string.hot_search_title), + false + ) + ) + list[layoutType - 6].check = true + return list +} + +fun womanList(layoutType: Int): List { + val list: MutableList = + ArrayList() + list.clear() + list.add(TypeName(LayoutType.GIRL_HOT, App.INSTANCE.getString(R.string.big_hot_title), false)) + list.add(TypeName(LayoutType.GIRL_END, App.INSTANCE.getString(R.string.end_title2), false)) + list.add( + TypeName( + LayoutType.GIRL_SEARCH, + App.INSTANCE.getString(R.string.hot_search_title), + false + ) + ) + list[layoutType - 9].check = true + return list +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/model/UserResp.kt b/app/src/main/java/com/novel/read/data/model/UserResp.kt new file mode 100644 index 0000000..942093e --- /dev/null +++ b/app/src/main/java/com/novel/read/data/model/UserResp.kt @@ -0,0 +1,32 @@ +package com.novel.read.data.model + +import com.novel.read.data.db.entity.User + +data class UserResp( + val user: User +) +// +//data class User( +// val createTime: Long, +// val email: String, +// val headImageUrl: String, +// val idToken: String, +// val introduction: Any, +// val ip: String, +// val isEmailVerified: Int, +// val lastLoginTime: Any, +// val nickName: String, +// val oldNickName: Any, +// val os: String, +// val password: String, +// val phone: String, +// val provider: String, +// val sex: Any, +// val updateTime: Any, +// val userId: Long, +// val userName: String, +// val validFlag: Int, +// val vipEndTime: Any, +// val vipStartTime: Any, +// val vipStatus: Int +//) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/read/TextChapter.kt b/app/src/main/java/com/novel/read/data/read/TextChapter.kt new file mode 100644 index 0000000..463321e --- /dev/null +++ b/app/src/main/java/com/novel/read/data/read/TextChapter.kt @@ -0,0 +1,54 @@ +package com.novel.read.data.read + +import kotlin.math.min + +data class TextChapter( + val position: Int, + val title: String, + val chapterId: Int, + val pages: List, + val pageLines: List, + val pageLengths: List, + val chaptersSize: Int +) { + fun page(index: Int): TextPage? { + return pages.getOrNull(index) + } + + val lastPage: TextPage? get() = pages.lastOrNull() + + val lastIndex: Int get() = pages.lastIndex + + val pageSize: Int get() = pages.size + + fun isLastIndex(index: Int): Boolean { + return index >= pages.size - 1 + } + + fun getReadLength(pageIndex: Int): Int { + var length = 0 + val maxIndex = min(pageIndex, pages.size) + for (index in 0 until maxIndex) { + length += pageLengths[index] + } + return length + } + + fun getUnRead(pageIndex: Int): String { + val stringBuilder = StringBuilder() + if (pages.isNotEmpty()) { + for (index in pageIndex..pages.lastIndex) { + stringBuilder.append(pages[index].text) + } + } + return stringBuilder.toString() + } + + fun getContent(): String { + val stringBuilder = StringBuilder() + pages.forEach { + stringBuilder.append(it.text) + } + return stringBuilder.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/read/TextChar.kt b/app/src/main/java/com/novel/read/data/read/TextChar.kt new file mode 100644 index 0000000..03400b3 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/read/TextChar.kt @@ -0,0 +1,9 @@ +package com.novel.read.data.read + +data class TextChar( + val charData: String, + var start: Float, + var end: Float, + var selected: Boolean = false, + var isImage: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/data/read/TextLine.kt b/app/src/main/java/com/novel/read/data/read/TextLine.kt new file mode 100644 index 0000000..237a252 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/read/TextLine.kt @@ -0,0 +1,39 @@ +package com.novel.read.data.read + +import android.text.TextPaint +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.ChapterProvider.textHeight + +data class TextLine( + var text: String = "", + val textChars: ArrayList = arrayListOf(), + var lineTop: Float = 0f, + var lineBase: Float = 0f, + var lineBottom: Float = 0f, + val isTitle: Boolean = false, + val isImage: Boolean = false, + var isReadAloud: Boolean = false +) { + + fun upTopBottom(durY: Float, textPaint: TextPaint) { + lineTop = ChapterProvider.paddingTop + durY + lineBottom = lineTop + textPaint.textHeight + lineBase = lineBottom - textPaint.fontMetrics.descent + } + + fun addTextChar(charData: String, start: Float, end: Float) { + textChars.add(TextChar(charData, start = start, end = end)) + } + + fun getTextCharAt(index: Int): TextChar { + return textChars[index] + } + + fun getTextCharReverseAt(index: Int): TextChar { + return textChars[textChars.lastIndex - index] + } + + fun getTextCharsCount(): Int { + return textChars.size + } +} diff --git a/app/src/main/java/com/novel/read/data/read/TextPage.kt b/app/src/main/java/com/novel/read/data/read/TextPage.kt new file mode 100644 index 0000000..25189e1 --- /dev/null +++ b/app/src/main/java/com/novel/read/data/read/TextPage.kt @@ -0,0 +1,122 @@ +package com.novel.read.data.read + +import android.text.Layout +import android.text.StaticLayout +import com.novel.read.App +import com.novel.read.R +import com.novel.read.help.ReadBookConfig +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import java.text.DecimalFormat + +data class TextPage( + var index: Int = 0, + var text: String = App.INSTANCE.getString(R.string.data_loading), + var title: String = "", + val textLines: ArrayList = arrayListOf(), + var pageSize: Int = 0, + var chapterSize: Int = 0, + var chapterIndex: Int = 0, + var height: Float = 0f +) { + + fun upLinesPosition() = ChapterProvider.apply { + if (!ReadBookConfig.textBottomJustify) return@apply + if (textLines.size <= 1) return@apply + if (textLines.last().isImage) return@apply + if (visibleHeight - height >= with(textLines.last()) { lineBottom - lineTop }) return@apply + val surplus = (visibleBottom - textLines.last().lineBottom) + if (surplus == 0f) return@apply + height += surplus + val tj = surplus / (textLines.size - 1) + for (i in 1 until textLines.size) { + val line = textLines[i] + line.lineTop = line.lineTop + tj * i + line.lineBase = line.lineBase + tj * i + line.lineBottom = line.lineBottom + tj * i + } + } + + @Suppress("DEPRECATION") + fun format(): TextPage { + if (textLines.isEmpty() && ChapterProvider.visibleWidth > 0) { + val layout = StaticLayout( + text, ChapterProvider.contentPaint, ChapterProvider.visibleWidth, + Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false + ) + var y = (ChapterProvider.visibleHeight - layout.height) / 2f + if (y < 0) y = 0f + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine() + textLine.lineTop = ChapterProvider.paddingTop + y + layout.getLineTop(lineIndex) + textLine.lineBase = + ChapterProvider.paddingTop + y + layout.getLineBaseline(lineIndex) + textLine.lineBottom = + ChapterProvider.paddingTop + y + layout.getLineBottom(lineIndex) + var x = ChapterProvider.paddingLeft + + (ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2 + textLine.text = + text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) + for (i in textLine.text.indices) { + val char = textLine.text[i].toString() + val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint) + val x1 = x + cw + textLine.addTextChar(charData = char, start = x, end = x1) + x = x1 + } + textLines.add(textLine) + } + height = ChapterProvider.visibleHeight.toFloat() + } + return this + } + + fun removePageAloudSpan(): TextPage { + textLines.forEach { textLine -> + textLine.isReadAloud = false + } + return this + } + + fun upPageAloudSpan(pageStart: Int) { + removePageAloudSpan() + var lineStart = 0 + for ((index, textLine) in textLines.withIndex()) { + if (pageStart > lineStart && pageStart < lineStart + textLine.text.length) { + for (i in index - 1 downTo 0) { + if (textLines[i].text.endsWith("\n")) { + break + } else { + textLines[i].isReadAloud = true + } + } + for (i in index until textLines.size) { + if (textLines[i].text.endsWith("\n")) { + textLines[i].isReadAloud = true + break + } else { + textLines[i].isReadAloud = true + } + } + break + } + lineStart += textLine.text.length + } + } + + val readProgress: String + get() { + val df = DecimalFormat("0.0%") + if (chapterSize == 0 || pageSize == 0 && chapterIndex == 0) { + return "0.0%" + } else if (pageSize == 0) { + return df.format((chapterIndex + 1.0f) / chapterSize.toDouble()) + } + var percent = + df.format(chapterIndex * 1.0f / chapterSize + 1.0f / chapterSize * (index + 1) / pageSize.toDouble()) + if (percent == "100.0%" && (chapterIndex + 1 != chapterSize || index + 1 != pageSize)) { + percent = "99.9%" + } + return percent + } + +} diff --git a/app/src/main/java/com/novel/read/event/AddBookSignEvent.kt b/app/src/main/java/com/novel/read/event/AddBookSignEvent.kt deleted file mode 100644 index 4566d2a..0000000 --- a/app/src/main/java/com/novel/read/event/AddBookSignEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.mango.mangolib.http.ErrorResponse - -/** - * create by 赵利君 on 2019/6/25 - * describe: - */ -class AddBookSignEvent : BaseEvent { - constructor(result: ErrorResponse) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/BookArticleEvent.kt b/app/src/main/java/com/novel/read/event/BookArticleEvent.kt deleted file mode 100644 index 9901bb1..0000000 --- a/app/src/main/java/com/novel/read/event/BookArticleEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.BookArticleResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class BookArticleEvent : BaseEvent { - - constructor(result: BookArticleResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/DeleteBookSignEvent.kt b/app/src/main/java/com/novel/read/event/DeleteBookSignEvent.kt deleted file mode 100644 index ee62dbd..0000000 --- a/app/src/main/java/com/novel/read/event/DeleteBookSignEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.mango.mangolib.http.ErrorResponse - -class DeleteBookSignEvent : BaseEvent{ - constructor(result: ErrorResponse) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/DeleteResponseEvent.kt b/app/src/main/java/com/novel/read/event/DeleteResponseEvent.kt deleted file mode 100644 index 1691041..0000000 --- a/app/src/main/java/com/novel/read/event/DeleteResponseEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.db.CollBookBean - -class DeleteResponseEvent(var isDelete: Boolean, var collBook: CollBookBean) : BaseEvent() - diff --git a/app/src/main/java/com/novel/read/event/DeleteTaskEvent.kt b/app/src/main/java/com/novel/read/event/DeleteTaskEvent.kt deleted file mode 100644 index 4474419..0000000 --- a/app/src/main/java/com/novel/read/event/DeleteTaskEvent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.db.CollBookBean - -class DeleteTaskEvent(var collBook: CollBookBean) : BaseEvent() diff --git a/app/src/main/java/com/novel/read/event/EditEvent.kt b/app/src/main/java/com/novel/read/event/EditEvent.kt deleted file mode 100644 index 5cf201a..0000000 --- a/app/src/main/java/com/novel/read/event/EditEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.RecommendListResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class EditEvent : BaseEvent { - - constructor(result: RecommendListResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/ErrorChapterEvent.kt b/app/src/main/java/com/novel/read/event/ErrorChapterEvent.kt deleted file mode 100644 index b65bead..0000000 --- a/app/src/main/java/com/novel/read/event/ErrorChapterEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent - -class ErrorChapterEvent : BaseEvent { - constructor(result: Any) : super(result) - - constructor() -} diff --git a/app/src/main/java/com/novel/read/event/FinishChapterEvent.kt b/app/src/main/java/com/novel/read/event/FinishChapterEvent.kt deleted file mode 100644 index d2d7a6c..0000000 --- a/app/src/main/java/com/novel/read/event/FinishChapterEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent - -class FinishChapterEvent : BaseEvent { - constructor(result: Any) : super(result) - - constructor() -} diff --git a/app/src/main/java/com/novel/read/event/GetBookDetailEvent.kt b/app/src/main/java/com/novel/read/event/GetBookDetailEvent.kt deleted file mode 100644 index d5bc678..0000000 --- a/app/src/main/java/com/novel/read/event/GetBookDetailEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.BookDetailResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class GetBookDetailEvent : BaseEvent { - - constructor(result: BookDetailResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/GetBookSignEvent.kt b/app/src/main/java/com/novel/read/event/GetBookSignEvent.kt deleted file mode 100644 index 9e8b03b..0000000 --- a/app/src/main/java/com/novel/read/event/GetBookSignEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.MarkResp - -/** - * create by 赵利君 on 2019/6/25 - * describe: - */ -class GetBookSignEvent : BaseEvent { - constructor(result: MarkResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/GetCategoryTypeEvent.kt b/app/src/main/java/com/novel/read/event/GetCategoryTypeEvent.kt deleted file mode 100644 index 88c8e64..0000000 --- a/app/src/main/java/com/novel/read/event/GetCategoryTypeEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.CategoryTypeResp - -/** - * create by zlj on 2019/6/18 - * describe: - */ -class GetCategoryTypeEvent : BaseEvent { - - constructor(result: CategoryTypeResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/GetRecommendBookEvent.kt b/app/src/main/java/com/novel/read/event/GetRecommendBookEvent.kt deleted file mode 100644 index fbdeff5..0000000 --- a/app/src/main/java/com/novel/read/event/GetRecommendBookEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.RecommendBookResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class GetRecommendBookEvent : BaseEvent { - - constructor(result: RecommendBookResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/HideBottomBarEvent.kt b/app/src/main/java/com/novel/read/event/HideBottomBarEvent.kt deleted file mode 100644 index a9f7469..0000000 --- a/app/src/main/java/com/novel/read/event/HideBottomBarEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent - -class HideBottomBarEvent(result: Boolean?) : BaseEvent(result!!) diff --git a/app/src/main/java/com/novel/read/event/HotSearchEvent.kt b/app/src/main/java/com/novel/read/event/HotSearchEvent.kt deleted file mode 100644 index 06b220e..0000000 --- a/app/src/main/java/com/novel/read/event/HotSearchEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.HotSearchResp -import com.novel.read.model.protocol.SearchResp - -/** - * create by zlj on 2019/6/18 - * describe: - */ -class HotSearchEvent : BaseEvent{ - - constructor(result: HotSearchResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/HumanEvent.kt b/app/src/main/java/com/novel/read/event/HumanEvent.kt deleted file mode 100644 index 43025b1..0000000 --- a/app/src/main/java/com/novel/read/event/HumanEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.novel.read.event - - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.RecommendListResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class HumanEvent : BaseEvent { - - constructor(result: RecommendListResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/LoginEvent.kt b/app/src/main/java/com/novel/read/event/LoginEvent.kt deleted file mode 100644 index 000e2bb..0000000 --- a/app/src/main/java/com/novel/read/event/LoginEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.UidResp - -class LoginEvent : BaseEvent { - constructor(result: UidResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/RankByUpdateEvent.kt b/app/src/main/java/com/novel/read/event/RankByUpdateEvent.kt deleted file mode 100644 index 4642e32..0000000 --- a/app/src/main/java/com/novel/read/event/RankByUpdateEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.RankByUpdateResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class RankByUpdateEvent : BaseEvent { - - constructor(result: RankByUpdateResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/ReStartEvent.java b/app/src/main/java/com/novel/read/event/ReStartEvent.java deleted file mode 100644 index 39d1224..0000000 --- a/app/src/main/java/com/novel/read/event/ReStartEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.novel.read.event; - -import com.mango.mangolib.event.BaseEvent; - -public class ReStartEvent extends BaseEvent { -} diff --git a/app/src/main/java/com/novel/read/event/RecommendListEvent.kt b/app/src/main/java/com/novel/read/event/RecommendListEvent.kt deleted file mode 100644 index c8fbf76..0000000 --- a/app/src/main/java/com/novel/read/event/RecommendListEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.RecommendListResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class RecommendListEvent : BaseEvent { - - constructor(result: RecommendListResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/RxBus.java b/app/src/main/java/com/novel/read/event/RxBus.java deleted file mode 100644 index 1cd13e3..0000000 --- a/app/src/main/java/com/novel/read/event/RxBus.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.novel.read.event; - -import io.reactivex.Observable; -import io.reactivex.subjects.PublishSubject; - -public class RxBus { - private static volatile RxBus sInstance; - private final PublishSubject mEventBus = PublishSubject.create(); - - public static RxBus getInstance(){ - if (sInstance == null){ - synchronized (RxBus.class){ - if (sInstance == null){ - sInstance = new RxBus(); - } - } - } - return sInstance; - } - - /** - * 发送事件(post event) - * @param event : event object(事件的内容) - */ - public void post(Object event){ - mEventBus.onNext(event); - } - - /** - * - * @param code - * @param event - */ - public void post(int code,Object event){ - Message msg = new Message(code,event); - mEventBus.onNext(msg); - } - - /** - * 返回Event的管理者,进行对事件的接受 - * @return - */ - public Observable toObservable(){ - return mEventBus; - } - - /** - * - * @param cls :保证接受到制定的类型 - * @param - * @return - */ - public Observable toObservable(Class cls){ - //ofType起到过滤的作用,确定接受的类型 - return mEventBus.ofType(cls); - } - - public Observable toObservable(int code,Class cls){ - return mEventBus.ofType(Message.class) - .filter(msg -> msg.code == code && cls.isInstance(msg.event)) - .map( msg -> (T)msg.event); - - } - - class Message{ - int code; - Object event; - - public Message(int code,Object event){ - this.code = code; - this.event = event; - } - } -} diff --git a/app/src/main/java/com/novel/read/event/SearchListEvent.kt b/app/src/main/java/com/novel/read/event/SearchListEvent.kt deleted file mode 100644 index 4376ab2..0000000 --- a/app/src/main/java/com/novel/read/event/SearchListEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.SearchResp - -/** - * create by 赵利君 on 2019/6/18 - * describe: - */ -class SearchListEvent : BaseEvent { - - constructor(result: SearchResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/SwitchFragmentEvent.kt b/app/src/main/java/com/novel/read/event/SwitchFragmentEvent.kt deleted file mode 100644 index 296ab2e..0000000 --- a/app/src/main/java/com/novel/read/event/SwitchFragmentEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent - -class SwitchFragmentEvent : BaseEvent { - - constructor(result: Any) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/UpdateBookEvent.kt b/app/src/main/java/com/novel/read/event/UpdateBookEvent.kt deleted file mode 100644 index 8f4a974..0000000 --- a/app/src/main/java/com/novel/read/event/UpdateBookEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent - -class UpdateBookEvent : BaseEvent { - - constructor(result: String) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/event/VersionEvent.kt b/app/src/main/java/com/novel/read/event/VersionEvent.kt deleted file mode 100644 index 6bfd821..0000000 --- a/app/src/main/java/com/novel/read/event/VersionEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.novel.read.event - -import com.mango.mangolib.event.BaseEvent -import com.novel.read.model.protocol.VersionResp - -/** - * create by 赵利君 on 2019/6/25 - * describe: - */ -class VersionEvent : BaseEvent { - constructor(result: VersionResp) : super(result) {} - - constructor() {} -} diff --git a/app/src/main/java/com/novel/read/fragment/BookFragment.kt b/app/src/main/java/com/novel/read/fragment/BookFragment.kt deleted file mode 100644 index 5c78b91..0000000 --- a/app/src/main/java/com/novel/read/fragment/BookFragment.kt +++ /dev/null @@ -1,277 +0,0 @@ -package com.novel.read.fragment - -import android.app.AlertDialog -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.appcompat.app.AppCompatDelegate -import androidx.recyclerview.widget.GridLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.activity.NovelMainActivity -import com.novel.read.activity.NovelSearchActivity -import com.novel.read.adapter.BookAdapter -import com.novel.read.base.NovelBaseFragment -import com.novel.read.constants.Constant -import com.novel.read.event.HideBottomBarEvent -import com.novel.read.event.SwitchFragmentEvent -import com.novel.read.event.UpdateBookEvent -import com.novel.read.http.AccountManager -import com.novel.read.model.db.BookRecordBean -import com.novel.read.model.db.CollBookBean -import com.novel.read.model.db.dbManage.BookRepository -import com.novel.read.model.protocol.BookDetailResp -import com.novel.read.showToast -import com.novel.read.utlis.LocalManageUtil -import com.novel.read.utlis.RxUtils -import com.novel.read.utlis.SpUtil -import com.squareup.otto.Subscribe -import io.reactivex.Single -import io.reactivex.SingleObserver -import io.reactivex.disposables.Disposable -import kotlinx.android.synthetic.main.fragment_book.* -import org.litepal.LitePal -import java.util.* - -/** - * create by 赵利君 on 2019/10/14 - * describe: - */ -class BookFragment : NovelBaseFragment() { - - private lateinit var mAdapter: BookAdapter - private val mList = ArrayList() - private var isInit = true - - override fun getLayoutId(): Int { - return R.layout.fragment_book - } - - override fun initView() { - EventManager.instance.registerSubscriber(this) - - mList.addAll(BookRepository.getInstance().collBooks) - val gridLayoutManager = GridLayoutManager(activity, 3) - gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(i: Int): Int { - return if (mList.size == 0) { - 3 - } else { - 1 - } - } - } - - rlv_book.layoutManager = gridLayoutManager - mAdapter = BookAdapter(mList) - rlv_book.adapter = mAdapter - if (isInit) { - isInit = false - update(mList) - } - } - - override fun initData() { - title.inflateMenu(R.menu.title_book) - title_edit.inflateMenu(R.menu.title_edit) - setOnClick() - } - - private fun setOnClick() { - title.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.action_search -> { - toActivity(NovelSearchActivity::class.java) - activity!!.overridePendingTransition( - R.anim.message_fade_in, - R.anim.message_fade_out - ) - } - R.id.edit_book -> if (mList.size == 0) { //没书的时候提醒用户不能编辑 - activity!!.showToast(getString(R.string.please_add_book)) - } else { - mAdapter.setEdit(true) - tv_cancel.visibility = View.VISIBLE - tv_delete.visibility = View.VISIBLE - title_edit.visibility = View.VISIBLE - title.visibility = View.GONE - EventManager.instance.postEvent(HideBottomBarEvent(true)) - } - R.id.book_sort -> showBookSortDialog() - R.id.menu2 -> showLanguageDialog() - R.id.menu3 -> { - if (SpUtil.getBooleanValue(Constant.NIGHT)) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - SpUtil.setBooleanValue(Constant.NIGHT, false) - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - SpUtil.setBooleanValue(Constant.NIGHT, true) - } - activity!!.recreate() - } - } - true - } - title_edit.setOnMenuItemClickListener { menuItem -> - if (menuItem.itemId == R.id.action_edit) { - for (i in mList.indices) { - mList[i].isSelect = true - } - mAdapter.notifyDataSetChanged() - } - true - } - tv_cancel.setOnClickListener { - EventManager.instance.postEvent(HideBottomBarEvent(false)) - updateBook(UpdateBookEvent()) - } - - tv_delete.setOnClickListener { - val deleteList = mAdapter.selectList - val iterator = deleteList.iterator() - while (iterator.hasNext()) { - val collBookBean = iterator.next() - if (collBookBean.isSaved) { - collBookBean.delete() - LitePal.deleteAll(BookRecordBean::class.java, "bookId=?", collBookBean.id) - } - } - activity!!.showToast(getString(R.string.delete_success)) - EventManager.instance.postEvent(HideBottomBarEvent(false)) - updateBook(UpdateBookEvent()) - } - - mAdapter.setOnItemClickListener(object : BookAdapter.OnItemClickListener { - override fun onItemClick(view: View, pos: Int) { - EventManager.instance.postEvent(SwitchFragmentEvent()) - } - }) - - - } - - - private fun showLanguageDialog() { - AlertDialog.Builder(activity) - .setTitle(getString(R.string.choose_language)) - .setSingleChoiceItems( - resources.getStringArray(R.array.setting_dialog_language_choice), - SpUtil.getIntValue(Constant.Language, 1) - ) { dialog, which -> - resources.getStringArray(R.array.setting_dialog_language_choice)[which] - SpUtil.setIntValue(Constant.Language, which) - dialog.dismiss() - if (which == 0) { - selectLanguage(0) - } else { - selectLanguage(1) - } - }.create().show() - } - - private fun selectLanguage(select: Int) { - LocalManageUtil.saveSelectLanguage(activity, select) - NovelMainActivity.reStart(activity!!) - } - - private fun showBookSortDialog() { - AlertDialog.Builder(activity) - .setTitle(getString(R.string.choose_language)) - .setSingleChoiceItems( - resources.getStringArray(R.array.setting_dialog_sort_choice), - if (SpUtil.getBooleanValue(Constant.BookSort, false)) 0 else 1 - ) { dialog, which -> - if (which == 0) { - SpUtil.setBooleanValue(Constant.BookSort, true) - } else { - SpUtil.setBooleanValue(Constant.BookSort, false) - } - updateBook(UpdateBookEvent()) - dialog.dismiss() - } - .create().show() - } - - override fun onResume() { - super.onResume() - updateBook(UpdateBookEvent()) - } - - @Subscribe - fun updateBook(event: UpdateBookEvent) { - tv_cancel.visibility = View.GONE - tv_delete.visibility = View.GONE - title.visibility = View.VISIBLE - title_edit.visibility = View.GONE - mList.clear() - mList.addAll(BookRepository.getInstance().collBooks) - mAdapter.setEdit(false) - } - - private fun update(collBookBeans: List?) { //检测书籍更新 - if (collBookBeans == null || collBookBeans.isEmpty()) return - val collBooks = ArrayList(collBookBeans) - val observables = ArrayList>(collBooks.size) - val it = collBooks.iterator() - while (it.hasNext()) { - val collBook = it.next() - //删除本地文件 - if (collBook.isLocal) { - it.remove() - } else { - observables.add(AccountManager.getInstance().getBookDetails(collBook.id)) - } - } - //zip可能不是一个好方法。 - Single.zip>(observables) { objects -> - val newCollBooks = ArrayList(objects.size) - for (i in collBooks.indices) { - val oldCollBook = collBooks[i] - val newCollBook = (objects[i] as BookDetailResp).collBookBean - //如果是oldBook是update状态,或者newCollBook与oldBook章节数不同 - if (oldCollBook.isUpdate || oldCollBook.lastChapter != newCollBook.lastChapter) { - newCollBook.setIsUpdate(true) - } else { - newCollBook.setIsUpdate(false) - } - newCollBook.lastRead = oldCollBook.lastRead - newCollBooks.add(newCollBook) - //存储到数据库中 - BookRepository.getInstance().saveCollBooks(newCollBooks) - } - newCollBooks - }.compose> { - RxUtils.toSimpleSingle( - it - ) - }.subscribe(object : SingleObserver> { - override fun onSubscribe(d: Disposable) { - } - - override fun onSuccess(value: List) { - //跟原先比较 - mList.clear() - mList.addAll(BookRepository.getInstance().collBooks) - mAdapter.notifyDataSetChanged() - } - - override fun onError(e: Throwable) {} - } - ) - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } - - companion object { - - fun newInstance(): BookFragment { - val args = Bundle() - val fragment = BookFragment() - fragment.arguments = args - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/fragment/BookListFragment.kt b/app/src/main/java/com/novel/read/fragment/BookListFragment.kt deleted file mode 100644 index a3ab24e..0000000 --- a/app/src/main/java/com/novel/read/fragment/BookListFragment.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.novel.read.fragment - -import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager -import com.novel.read.R -import com.novel.read.adapter.RankListAdapter -import com.novel.read.base.NovelBaseFragment -import com.novel.read.constants.Constant -import com.novel.read.constants.Constant.COMMENT_SIZE -import com.novel.read.http.AccountManager -import com.novel.read.inter.OnLoadMoreListener -import com.novel.read.model.protocol.RankByUpdateResp -import kotlinx.android.synthetic.main.fragment_book_list.* -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.util.* - - -class BookListFragment : NovelBaseFragment() { - - private lateinit var mAdapter: RankListAdapter - private var mList: MutableList = ArrayList() - private var sex: String = "" - private var dateType: String = "" - private var type: String = "" - private var page = 1 - private var loadSize: Int = 0 - - override fun getLayoutId(): Int { - return R.layout.fragment_book_list - } - - override fun initView() { - rlv_book_list.layoutManager = LinearLayoutManager(activity) - mAdapter = RankListAdapter(mList, rlv_book_list) - rlv_book_list.adapter = mAdapter - - arguments?.let { - sex = it.getString(Constant.Sex) - dateType = it.getString(Constant.DateType) - type = it.getString(Constant.Type) - } - - } - - override fun initData() { - getData() - - mAdapter.setOnLoadMoreListener(object : OnLoadMoreListener { - override fun onLoadMore() { - if (mAdapter.isLoadingMore) { - - } else { - if (loadSize >= COMMENT_SIZE) { - mAdapter.isLoadingMore = true - mAdapter.notifyDataSetChanged() - page++ - getData() - } - } - } - }) - - } - - private fun getData() { - AccountManager.getInstance().getRankList(type, sex, dateType, page.toString(), RankCallBack()) - } - - private inner class RankCallBack : Callback { - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - if (response.body() != null) { - loadSize = response.body()!!.book.size - if (mAdapter.isLoadingMore) { - mAdapter.isLoadingMore = false - mList.addAll(response.body()!!.book) - mAdapter.notifyDataSetChanged() - } else { - mList.clear() - mList.addAll(response.body()!!.book) - mAdapter.notifyDataSetChanged() - } - } - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } - - companion object { - - fun newInstance(type: String, dateType: String, sex: String): BookListFragment { - val args = Bundle() - args.putString(Constant.Sex, sex) - args.putString(Constant.DateType, dateType) - args.putString(Constant.Type, type) - val fragment = BookListFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/app/src/main/java/com/novel/read/fragment/ManFragment.kt b/app/src/main/java/com/novel/read/fragment/ManFragment.kt deleted file mode 100644 index 5b9ede5..0000000 --- a/app/src/main/java/com/novel/read/fragment/ManFragment.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.novel.read.fragment - -import android.os.Bundle -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.adapter.EditRecommendAdapter -import com.novel.read.adapter.HumanAdapter -import com.novel.read.adapter.RankAdapter -import com.novel.read.base.NovelBaseFragment -import com.novel.read.constants.Constant -import com.novel.read.dp2px -import com.novel.read.http.AccountManager -import com.novel.read.model.protocol.RecommendListResp -import com.novel.read.utlis.EvenItemDecoration -import kotlinx.android.synthetic.main.fragment_man.* -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.util.* - - -class ManFragment : NovelBaseFragment() { - - private lateinit var mHumanAdapter: HumanAdapter - private val mHumanList = ArrayList() - private lateinit var mEditAdapter: EditRecommendAdapter - private val mEditList = ArrayList() - private lateinit var mRankAdapter: RankAdapter - private val mRankList = ArrayList() - - override fun getLayoutId(): Int { - return R.layout.fragment_man - } - - override fun initView() { - EventManager.instance.registerSubscriber(this) - - mHumanAdapter = HumanAdapter(mHumanList) - rlv_pop.apply { - layoutManager = GridLayoutManager(activity, 3) - adapter = mHumanAdapter - addItemDecoration(EvenItemDecoration(activity!!.dp2px(10), 3)) - } - - val linearLayoutManager = LinearLayoutManager(activity) - linearLayoutManager.orientation = LinearLayoutManager.HORIZONTAL - rlv_recommend.layoutManager = linearLayoutManager - mEditAdapter = EditRecommendAdapter(mEditList) - rlv_recommend.adapter = mEditAdapter - - mRankAdapter = RankAdapter(mRankList) - rlv_update.apply { - layoutManager = GridLayoutManager(activity, 3) - adapter = mRankAdapter - addItemDecoration(EvenItemDecoration(activity!!.dp2px(10), 3)) - } - } - - override fun initData() { - swipe.showLoading() - getData() - swipe.setOnReloadingListener { getData() } - } - - private fun getData() { - AccountManager.getInstance().getRecommendList(Constant.ListType.Human, HumanCallBack()) - AccountManager.getInstance() - .getRecommendList(Constant.ListType.EditRecommend, EditCallBack()) - AccountManager.getInstance() - .getRecommendList(Constant.ListType.HotSearch, HotSearchCallBack()) - } - - private inner class HumanCallBack : Callback { - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - mHumanList.clear() - mHumanList.addAll(response.body()!!.list) - mHumanAdapter.notifyDataSetChanged() - } else { - swipe.showError() - } - } - - override fun onFailure(call: Call, t: Throwable) { - swipe.showError() - } - } - - private inner class EditCallBack : Callback { - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - mEditList.clear() - mEditList.addAll(response.body()!!.list) - mEditAdapter.notifyDataSetChanged() - - } else { - swipe.showError() - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } - - private inner class HotSearchCallBack : Callback { - - override fun onResponse(call: Call, response: Response) { - swipe.showFinish() - if (response.isSuccessful && response.body() != null) { - mRankList.clear() - mRankList.addAll(response.body()!!.list) - mRankAdapter.notifyDataSetChanged() - } else { - swipe.showError() - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } - - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } - - companion object { - - fun newInstance(sex: String): ManFragment { - val args = Bundle() - args.putString(Constant.Sex, sex) - val fragment = ManFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/app/src/main/java/com/novel/read/fragment/MoreFragment.kt b/app/src/main/java/com/novel/read/fragment/MoreFragment.kt deleted file mode 100644 index 1b55bb8..0000000 --- a/app/src/main/java/com/novel/read/fragment/MoreFragment.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.novel.read.fragment - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.activity.NovelSearchActivity -import com.novel.read.activity.NovelSettingActivity -import com.novel.read.base.NovelBaseFragment -import com.novel.read.constants.Constant -import com.novel.read.event.ReStartEvent -import com.novel.read.showToast -import com.novel.read.utlis.VersionUtil -import com.novel.read.widget.dialog.AppraiseDialog -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.fragment_more.* - -/** - * create by 赵利君 on 2019/6/10 - * describe: - */ -class MoreFragment : NovelBaseFragment() { - - override fun getLayoutId(): Int { - return R.layout.fragment_more - } - - override fun initView() { - EventManager.instance.registerSubscriber(this) - toolbar.inflateMenu(R.menu.title_more) - } - - override fun initData() { - toolbar.setOnMenuItemClickListener { menuItem -> - if (menuItem.itemId == R.id.action_search) { - toActivity(NovelSearchActivity::class.java) - activity!!.overridePendingTransition( - R.anim.message_fade_in, - R.anim.message_fade_out - ) - } - true - } - - //意见反馈 - tv_options.setOnClickListener { - feedback() - } - - //评价 - tv_appraise.setOnClickListener { - val dialog = AppraiseDialog(activity!!) - dialog.appraiseDialog(View.OnClickListener { - val uri = Uri.parse("https://github.com/390057892/reader") - val intent = Intent(Intent.ACTION_VIEW, uri) - startActivity(intent) - dialog.dismiss() - }) - dialog.show() - } - - //设置 - tv_setting.setOnClickListener { - toActivity(NovelSettingActivity::class.java) - } - } - - private fun feedback() { - val email = Intent(Intent.ACTION_SEND) - //邮件发送类型:无附件,纯文本 - email.type = "plain/text" - //邮件接收者(数组,可以是多位接收者) - val emailReceiver = arrayOf(Constant.FeedBackEmail) - val emailTitle = getString(R.string.opinions) - val emailContent = "" - //设置邮件地址 - email.putExtra(Intent.EXTRA_EMAIL, emailReceiver) - //设置邮件标题 - email.putExtra(Intent.EXTRA_SUBJECT, emailTitle) - //设置发送的内容 - email.putExtra(Intent.EXTRA_TEXT, emailContent) - //调用系统的邮件系统 - startActivity(Intent.createChooser(email, "请选择邮件发送软件")) - - } - - - @Subscribe - fun restart(event: ReStartEvent) { - activity!!.recreate() - } - - companion object { - fun newInstance(): MoreFragment { - val args = Bundle() - val fragment = MoreFragment() - fragment.arguments = args - return fragment - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } -} diff --git a/app/src/main/java/com/novel/read/fragment/RecommendFragment.kt b/app/src/main/java/com/novel/read/fragment/RecommendFragment.kt deleted file mode 100644 index 337a8a5..0000000 --- a/app/src/main/java/com/novel/read/fragment/RecommendFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.novel.read.fragment - -import android.os.Bundle -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.activity.NovelSearchActivity -import com.novel.read.base.NovelBaseFragment -import kotlinx.android.synthetic.main.title_recommend.* - -/** - * create by zlj on 2019/6/10 - * describe: - */ -class RecommendFragment : NovelBaseFragment() { - - override fun getLayoutId(): Int { - return R.layout.fragment_recommend - } - - override fun initView() { - EventManager.instance.registerSubscriber(this) - // List fragmentList = new ArrayList<>(); - // ManFragment manFragment = ManFragment.newInstance(Constant.GenderType.Man); - // WomanFragment womanFragment = WomanFragment.newInstance(Constant.GenderType.Woman); - // fragmentList.add(manFragment); - // fragmentList.add(womanFragment); - } - - override fun initData() { - tv_search.setOnClickListener { - toActivity(NovelSearchActivity::class.java) - activity!!.overridePendingTransition( - R.anim.message_fade_in, - R.anim.message_fade_out - ) - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } - - companion object { - fun newInstance(): RecommendFragment { - val args = Bundle() - val fragment = RecommendFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/app/src/main/java/com/novel/read/fragment/StackFragment.kt b/app/src/main/java/com/novel/read/fragment/StackFragment.kt deleted file mode 100644 index 37fb7c1..0000000 --- a/app/src/main/java/com/novel/read/fragment/StackFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.novel.read.fragment - -import android.os.Bundle -import androidx.recyclerview.widget.GridLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.activity.NovelSearchActivity -import com.novel.read.adapter.StackAdapter -import com.novel.read.base.NovelBaseFragment -import com.novel.read.event.GetCategoryTypeEvent -import com.novel.read.http.AccountManager -import com.novel.read.model.protocol.CategoryTypeResp -import com.squareup.otto.Subscribe -import kotlinx.android.synthetic.main.fragment_stack.* -import kotlinx.android.synthetic.main.title_stack.* -import java.util.* - -/** - * create by zlj on 2019/6/10 - * describe: - */ -class StackFragment : NovelBaseFragment() { - - private lateinit var mAdapter: StackAdapter - private var mList: MutableList = ArrayList() - - override fun getLayoutId(): Int { - return R.layout.fragment_stack - } - - override fun initView() { - EventManager.instance.registerSubscriber(this) - rlv_book_type.layoutManager = GridLayoutManager(activity, 2) - mAdapter = StackAdapter(mList) - rlv_book_type.adapter = mAdapter - } - - override fun initData() { - refresh.showLoading() - getData() - refresh.setOnReloadingListener { getData() } - - tv_search.setOnClickListener { - toActivity(NovelSearchActivity::class.java) - activity!!.overridePendingTransition(R.anim.message_fade_in, R.anim.message_fade_out) - } - } - - private fun getData() { - AccountManager.getInstance().getCategoryType() - } - - @Subscribe - fun getCategoryType(event: GetCategoryTypeEvent) { - refresh.showFinish() - if (event.isFail) { - refresh.showError() - } else { - mList.clear() - mList.addAll(event.result!!.category) - mAdapter.notifyDataSetChanged() - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } - - companion object { - fun newInstance(): StackFragment { - val args = Bundle() - val fragment = StackFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/app/src/main/java/com/novel/read/fragment/WomanFragment.kt b/app/src/main/java/com/novel/read/fragment/WomanFragment.kt deleted file mode 100644 index 89d3278..0000000 --- a/app/src/main/java/com/novel/read/fragment/WomanFragment.kt +++ /dev/null @@ -1,171 +0,0 @@ -package com.novel.read.fragment - -import android.os.Bundle -import android.view.View -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import com.mango.mangolib.event.EventManager -import com.novel.read.R -import com.novel.read.activity.NovelRankListActivity -import com.novel.read.activity.NovelRecommendBookListActivity -import com.novel.read.adapter.EditRecommendAdapter -import com.novel.read.adapter.HumanAdapter -import com.novel.read.adapter.RankAdapter -import com.novel.read.base.NovelBaseFragment -import com.novel.read.constants.Constant -import com.novel.read.http.AccountManager -import com.novel.read.model.protocol.RecommendListResp -import kotlinx.android.synthetic.main.fragment_man.* -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.util.* - - -class WomanFragment : NovelBaseFragment(), View.OnClickListener { - - private lateinit var mHumanAdapter: HumanAdapter - private val mHumanList = ArrayList() - private lateinit var mEditAdapter: EditRecommendAdapter - private val mEditList = ArrayList() - private lateinit var mRankAdapter: RankAdapter - private val mRankList = ArrayList() - - override fun getLayoutId(): Int { - return R.layout.fragment_man - } - - override fun initView() { - EventManager.instance.registerSubscriber(this) - - rlv_pop.layoutManager = GridLayoutManager(activity, 3) - mHumanAdapter = HumanAdapter(mHumanList) - rlv_pop.adapter = mHumanAdapter - - val linearLayoutManager = LinearLayoutManager(activity) - linearLayoutManager.orientation = LinearLayoutManager.HORIZONTAL - rlv_recommend.layoutManager = linearLayoutManager - mEditAdapter = EditRecommendAdapter(mEditList) - rlv_recommend.adapter = mEditAdapter - - rlv_update.layoutManager = GridLayoutManager(activity, 3) - mRankAdapter = RankAdapter(mRankList) - rlv_update.adapter = mRankAdapter - - } - - override fun initData() { - arguments?.let { - val sex = it.getString(Constant.Sex) - swipe.showLoading() - getData(sex) - swipe.setOnReloadingListener { getData(sex) } - } - - } - - private fun getData(sex: String?) { - AccountManager.getInstance().getRecommendList(Constant.ListType.Human, HumanCallBack()) - AccountManager.getInstance() - .getRecommendList(Constant.ListType.EditRecommend, EditCallBack()) - AccountManager.getInstance() - .getRecommendList(Constant.ListType.HotSearch, HotSearchCallBack()) - } - - override fun onClick(v: View) { - val bundle = Bundle() - bundle.putString(Constant.Sex, Constant.GenderType.Woman) - when (view?.id) { - R.id.head_pop -> { - bundle.putString(Constant.Type, Constant.ListType.Human) - toActivity(NovelRecommendBookListActivity::class.java, bundle) - } - R.id.head_recommend -> { - bundle.putString(Constant.Type, Constant.ListType.EditRecommend) - toActivity(NovelRankListActivity::class.java, bundle) - } - R.id.head_update -> { - bundle.putString(Constant.Type, Constant.ListType.HotSearch) - toActivity(NovelRankListActivity::class.java, bundle) - } - } - } - - private inner class HumanCallBack : Callback { - - override fun onResponse( - call: Call, - response: Response - ) { - if (response.isSuccessful && response.body() != null) { - mHumanList.clear() - mHumanList.addAll(response.body()!!.list) - mHumanAdapter.notifyDataSetChanged() - - } else { - swipe.showError() - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } - - private inner class EditCallBack : Callback { - - override fun onResponse( - call: Call, - response: Response - ) { - if (response.isSuccessful && response.body() != null) { - mEditList.clear() - mEditList.addAll(response.body()!!.list) - mEditAdapter.notifyDataSetChanged() - - } else { - swipe.showError() - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } - - private inner class HotSearchCallBack : Callback { - - override fun onResponse( - call: Call, - response: Response - ) { - swipe.showFinish() - if (response.isSuccessful && response.body() != null) { - mRankList.clear() - mRankList.addAll(response.body()!!.list) - mRankAdapter.notifyDataSetChanged() - } else { - swipe.showError() - } - } - - override fun onFailure(call: Call, t: Throwable) { - - } - } - - override fun onDestroy() { - super.onDestroy() - EventManager.instance.unregisterSubscriber(this) - } - - companion object { - fun newInstance(sex: String): WomanFragment { - val args = Bundle() - args.putString(Constant.Sex, sex) - val fragment = WomanFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/app/src/main/java/com/novel/read/help/ActivityHelp.kt b/app/src/main/java/com/novel/read/help/ActivityHelp.kt new file mode 100644 index 0000000..b9a265f --- /dev/null +++ b/app/src/main/java/com/novel/read/help/ActivityHelp.kt @@ -0,0 +1,119 @@ +package com.novel.read.help + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import com.novel.read.utils.LanguageUtils +import java.lang.ref.WeakReference +import java.util.* + +/** + * Activity管理器,管理项目中Activity的状态 + */ +@Suppress("unused") +object ActivityHelp : Application.ActivityLifecycleCallbacks { + + private val activities: MutableList> = arrayListOf() + + fun size(): Int { + return activities.size + } + + /** + * 判断指定Activity是否存在 + */ + fun isExist(activityClass: Class<*>): Boolean { + activities.forEach { item -> + if (item.get()?.javaClass == activityClass) { + return true + } + } + return false + } + + /** + * 添加Activity + */ + fun add(activity: Activity) { + activities.add(WeakReference(activity)) + } + + /** + * 移除Activity + */ + fun remove(activity: Activity) { + for (temp in activities) { + if (null != temp.get() && temp.get() === activity) { + activities.remove(temp) + break + } + } + } + + /** + * 移除Activity + */ + fun remove(activityClass: Class<*>) { + val iterator = activities.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (item.get()?.javaClass == activityClass) { + iterator.remove() + } + } + } + + /** + * 关闭指定 activity + */ + fun finishActivity(vararg activities: Activity) { + activities.forEach { activity -> + activity.finish() + } + } + + /** + * 关闭指定 activity(class) + */ + fun finishActivity(vararg activityClasses: Class<*>) { + val waitFinish = ArrayList>() + for (temp in activities) { + for (activityClass in activityClasses) { + if (temp.get()?.javaClass == activityClass) { + waitFinish.add(temp) + break + } + } + } + waitFinish.forEach { + it.get()?.finish() + } + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityStarted(activity: Activity) { + + } + + override fun onActivityDestroyed(activity: Activity) { + remove(activity) + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + add(activity) + if (!LanguageUtils.isSameWithSetting(activity)){ + LanguageUtils.setConfiguration(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/AppConfig.kt b/app/src/main/java/com/novel/read/help/AppConfig.kt new file mode 100644 index 0000000..24dc0b1 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/AppConfig.kt @@ -0,0 +1,115 @@ +package com.novel.read.help + +import android.annotation.SuppressLint +import android.content.Context +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.PreferKey +import com.novel.read.utils.ext.* + +object AppConfig { + + var isEInkMode: Boolean = false + + fun isNightTheme(context: Context): Boolean { + return when (context.getPrefString(PreferKey.themeMode, "0")) { + "1" -> false + "2" -> true + "3" -> false + else -> context.sysIsDarkMode() + } + } + + var isNightTheme: Boolean + get() = isNightTheme(App.INSTANCE) + set(value) { + if (isNightTheme != value) { + if (value) { + App.INSTANCE.putPrefString(PreferKey.themeMode, "2") + } else { + App.INSTANCE.putPrefString(PreferKey.themeMode, "1") + } + } + } + + fun upEInkMode() { + isEInkMode = App.INSTANCE.getPrefString(PreferKey.themeMode) == "3" + } + + var isTransparentStatusBar: Boolean + get() = App.INSTANCE.getPrefBoolean(PreferKey.transparentStatusBar,true) + set(value) { + App.INSTANCE.putPrefBoolean(PreferKey.transparentStatusBar, value) + } + + val requestedDirection: String? + get() = App.INSTANCE.getPrefString(R.string.pk_requested_direction) + + var backupPath: String? + get() = App.INSTANCE.getPrefString(PreferKey.backupPath) + set(value) { + if (value.isNullOrEmpty()) { + App.INSTANCE.removePref(PreferKey.backupPath) + } else { + App.INSTANCE.putPrefString(PreferKey.backupPath, value) + } + } + + val autoRefreshBook: Boolean + get() = App.INSTANCE.getPrefBoolean(R.string.pk_auto_refresh) + + var threadCount: Int + get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.threadCount, value) + } + + var importBookPath: String? + get() = App.INSTANCE.getPrefString("importBookPath") + set(value) { + if (value == null) { + App.INSTANCE.removePref("importBookPath") + } else { + App.INSTANCE.putPrefString("importBookPath", value) + } + } + + var ttsSpeechRate: Int + get() = App.INSTANCE.getPrefInt(PreferKey.ttsSpeechRate, 5) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value) + } + + val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false) + + var chineseConverterType: Int + get() = App.INSTANCE.getPrefInt(PreferKey.chineseConverterType,2) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value) + } + + var systemTypefaces: Int + get() = App.INSTANCE.getPrefInt(PreferKey.systemTypefaces) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value) + } + + var elevation: Int + @SuppressLint("PrivateResource") + get() = App.INSTANCE.getPrefInt( + PreferKey.barElevation, +// App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt() + 0.toInt() + ) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.barElevation, value) + } + + var replaceEnableDefault: Boolean = + App.INSTANCE.getPrefBoolean(PreferKey.replaceEnableDefault, true) + + + val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true) + +} + diff --git a/app/src/main/java/com/novel/read/help/BookHelp.kt b/app/src/main/java/com/novel/read/help/BookHelp.kt new file mode 100644 index 0000000..a1e693b --- /dev/null +++ b/app/src/main/java/com/novel/read/help/BookHelp.kt @@ -0,0 +1,397 @@ +package com.novel.read.help + +import android.util.Log +import com.hankcs.hanlp.HanLP +import com.novel.read.App +import com.novel.read.constant.AppPattern +import com.novel.read.constant.EventBus +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.utils.FileUtils +import com.novel.read.utils.MD5Utils +import com.novel.read.utils.StringUtils +import com.novel.read.utils.ext.* +import com.novel.read.help.coroutine.Coroutine +import com.novel.read.network.repository.ImageRepository +import com.novel.read.utils.NetworkUtils +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import net.ricecode.similarity.JaroWinklerStrategy +import net.ricecode.similarity.StringSimilarityServiceImpl +import org.jetbrains.anko.toast +import java.io.File +import java.util.concurrent.CopyOnWriteArraySet +import java.util.regex.Matcher +import java.util.regex.Pattern +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +object BookHelp { + private const val cacheFolderName = "book_cache" + private const val cacheImageFolderName = "images" + private val downloadDir: File = App.INSTANCE.externalFilesDir + private val downloadImages = CopyOnWriteArraySet() + private val imageRepository by lazy { ImageRepository() } + fun formatChapterName(bookChapter: BookChapter): String { + return String.format( + "%05d-%s.nb", + bookChapter.chapterId, + MD5Utils.md5Encode16(bookChapter.chapterName) + ) + } + + fun clearCache() { + FileUtils.deleteFile( + FileUtils.getPath(downloadDir, cacheFolderName) + ) + } + + fun clearCache(book: Book) { + val filePath = FileUtils.getPath(downloadDir, cacheFolderName, book.getFolderName()) + FileUtils.deleteFile(filePath) + } + + /** + * 清楚已删除书的缓存 + */ + fun clearRemovedCache() { + Coroutine.async { + val bookFolderNames = arrayListOf() + App.db.getBookDao().getAllBooks().forEach { + bookFolderNames.add(it.getFolderName()) + } + val file = FileUtils.getFile(downloadDir, cacheFolderName) + file.listFiles()?.forEach { bookFile -> + if (!bookFolderNames.contains(bookFile.name)) { + FileUtils.deleteFile(bookFile.absolutePath) + } + } + } + } + + suspend fun saveContent(book: Book, bookChapter: BookChapter, content: String) { + if (content.isEmpty()) return + //保存文本 + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + formatChapterName(bookChapter) + ).writeText(content) + //保存图片 + content.split("\n").forEach { + val matcher = AppPattern.imgPattern.matcher(it) + if (matcher.find()) { + var src = matcher.group(1) + src = NetworkUtils.getAbsoluteURL(bookChapter.chapterUrl, src) + src?.let { + saveImage(book, src) + } + } + } + postEvent(EventBus.SAVE_CONTENT, bookChapter) + } + + suspend fun saveImage(book: Book, src: String) { + while (downloadImages.contains(src)) { + delay(100) + } + if (getImage(book, src).exists()) { + return + } + Log.e("saveImage", "saveImage: $src", ) + downloadImages.add(src) + try { + imageRepository.getImage(src).let { + val c=it + Log.e("saveImage", "bytes: $c", ) + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + cacheImageFolderName, + "${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}" + ).writeBytes(c.bytes()) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + downloadImages.remove(src) + } + } + + fun getImage(book: Book, src: String): File { + return FileUtils.getFile( + downloadDir, + cacheFolderName, + book.getFolderName(), + cacheImageFolderName, + "${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}" + ) + } + + private fun getImageSuffix(src: String): String { + var suffix = src.substringAfterLast(".").substringBefore(",") + if (suffix.length > 5) { + suffix = ".jpg" + } + return suffix + } + + fun getChapterFiles(book: Book): List { + val fileNameList = arrayListOf() + if (book.isLocalBook()) { + return fileNameList + } + FileUtils.createFolderIfNotExist( + downloadDir, + subDirs = arrayOf(cacheFolderName, book.getFolderName()) + ).list()?.let { + fileNameList.addAll(it) + } + return fileNameList + } + + // 检测该章节是否下载 + fun hasContent(book: Book, bookChapter: BookChapter): Boolean { + return if (book.isLocalBook()) { + true + } else { + FileUtils.exists( + downloadDir, + cacheFolderName, + book.getFolderName(), + formatChapterName(bookChapter) + ) + } + } + + fun getContent(book: Book, bookChapter: BookChapter): String? { + if (book.isLocalBook()) { +// return LocalBook.getContext(book, bookChapter) + //todo epub + return null + } else { + val file = FileUtils.getFile( + downloadDir, + cacheFolderName, + book.getFolderName(), + formatChapterName(bookChapter) + ) + if (file.exists()) { + return file.readText() + } + } + return null + } + + fun delContent(book: Book, bookChapter: BookChapter) { + if (book.isLocalBook()) { + return + } else { + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + formatChapterName(bookChapter) + ).delete() + } + } + + fun formatBookName(name: String): String { + return name + .replace(AppPattern.nameRegex, "") + .trim { it <= ' ' } + } + + fun formatBookAuthor(author: String): String { + return author + .replace(AppPattern.authorRegex, "") + .trim { it <= ' ' } + } + + /** + * 根据目录名获取当前章节 + */ + fun getDurChapter( + oldDurChapterIndex: Int, + oldChapterListSize: Int, + oldDurChapterName: String?, + newChapterList: List + ): Int { + if (oldChapterListSize == 0) return 0 + val oldChapterNum = getChapterNum(oldDurChapterName) + val oldName = getPureChapterName(oldDurChapterName) + val newChapterSize = newChapterList.size + val min = max( + 0, + min( + oldDurChapterIndex, + oldDurChapterIndex - oldChapterListSize + newChapterSize + ) - 10 + ) + val max = min( + newChapterSize - 1, + max( + oldDurChapterIndex, + oldDurChapterIndex - oldChapterListSize + newChapterSize + ) + 10 + ) + var nameSim = 0.0 + var newIndex = 0 + var newNum = 0 + if (oldName.isNotEmpty()) { + val service = StringSimilarityServiceImpl(JaroWinklerStrategy()) + for (i in min..max) { + val newName = getPureChapterName(newChapterList[i].chapterName) + val temp = service.score(oldName, newName) + if (temp > nameSim) { + nameSim = temp + newIndex = i + } + } + } + if (nameSim < 0.96 && oldChapterNum > 0) { + for (i in min..max) { + val temp = getChapterNum(newChapterList[i].chapterName) + if (temp == oldChapterNum) { + newNum = temp + newIndex = i + break + } else if (abs(temp - oldChapterNum) < abs(newNum - oldChapterNum)) { + newNum = temp + newIndex = i + } + } + } + return if (nameSim > 0.96 || abs(newNum - oldChapterNum) < 1) { + newIndex + } else { + min(max(0, newChapterList.size - 1), oldDurChapterIndex) + } + } + + private val chapterNamePattern by lazy { + Pattern.compile("^(.*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟0-9\\s]+)[章节篇回集])[、,。 ::.\\s]*") + } + + private fun getChapterNum(chapterName: String?): Int { + if (chapterName != null) { + val matcher: Matcher = chapterNamePattern.matcher(chapterName) + if (matcher.find()) { + return StringUtils.stringToInt(matcher.group(2)) + } + } + return -1 + } + + @Suppress("SpellCheckingInspection") + private val regexOther by lazy { + // 所有非字母数字中日韩文字 CJK区+扩展A-F区 + return@lazy "[^\\w\\u4E00-\\u9FEF〇\\u3400-\\u4DBF\\u20000-\\u2A6DF\\u2A700-\\u2EBEF]".toRegex() + } + + private val regexA by lazy { + return@lazy "\\s".toRegex() + } + + private val regexB by lazy { + return@lazy "^第.*?章|[(\\[][^()\\[\\]]{2,}[)\\]]$".toRegex() + } + + private fun getPureChapterName(chapterName: String?): String { + return if (chapterName == null) "" else StringUtils.fullToHalf(chapterName) + .replace(regexA, "") + .replace(regexB, "") + .replace(regexOther, "") + } + + private var bookName: String? = null + private var bookOrigin: String? = null +// private var replaceRules: List = arrayListOf() + +// @Synchronized +// fun upReplaceRules() { +// val o = bookOrigin +// bookName?.let { +// replaceRules = if (o.isNullOrEmpty()) { +// App.db.replaceRuleDao().findEnabledByScope(it) +// } else { +// App.db.replaceRuleDao().findEnabledByScope(it, o) +// } +// } +// } + + suspend fun disposeContent( + book: Book, + title: String, + content: String + ): List { + var title1 = title + var content1 = content +// if (book.getReSegment()) { +// content1 = ContentHelp.reSegment(content1, title1) +// } +// if (book.getUseReplaceRule()) { +// synchronized(this) { +// if (bookName != book.name || bookOrigin != book.origin) { +// bookName = book.name +// bookOrigin = book.origin +// replaceRules = if (bookOrigin.isNullOrEmpty()) { +// App.db.replaceRuleDao().findEnabledByScope(bookName!!) +// } else { +// App.db.replaceRuleDao().findEnabledByScope(bookName!!, bookOrigin!!) +// } +// } +// } +// replaceRules.forEach { item -> +// item.pattern.let { +// if (it.isNotEmpty()) { +// try { +// content1 = if (item.isRegex) { +// content1.replace(it.toRegex(), item.replacement) +// } else { +// content1.replace(it, item.replacement) +// } +// } catch (e: Exception) { +// withContext(Main) { +// App.INSTANCE.toast("${item.name}替换出错") +// } +// } +// } +// } +// } +// } + try { + when (AppConfig.chineseConverterType) { + 1 -> { + title1 = HanLP.convertToSimplifiedChinese(title1) + content1 = HanLP.convertToSimplifiedChinese(content1) + } + 2 -> { + title1 = HanLP.convertToTraditionalChinese(title1) + content1 = HanLP.convertToTraditionalChinese(content1) + } + } + } catch (e: Exception) { + withContext(Main) { + App.INSTANCE.toast("简繁转换出错") + } + } + val contents = arrayListOf() + content1.split("\n").forEach { + val str = it.replace("^[\\n\\s\\r]+".toRegex(), "") + if (contents.isEmpty()) { + contents.add(title1) + if (str != title1 && str.isNotEmpty()) { + contents.add("${ReadBookConfig.paragraphIndent}$str") + } + } else if (str.isNotEmpty()) { + contents.add("${ReadBookConfig.paragraphIndent}$str") + } + } + return contents + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/DefaultData.kt b/app/src/main/java/com/novel/read/help/DefaultData.kt new file mode 100644 index 0000000..9f9ad9d --- /dev/null +++ b/app/src/main/java/com/novel/read/help/DefaultData.kt @@ -0,0 +1,37 @@ +package com.novel.read.help + +import com.novel.read.App +import com.novel.read.data.db.entity.HttpTTS +import com.novel.read.utils.ext.* +import java.io.File + +object DefaultData { + + const val httpTtsFileName = "httpTTS.json" + const val txtTocRuleFileName = "txtTocRule.json" + + val defaultHttpTTS by lazy { + val json = + String( + App.INSTANCE.assets.open("defaultData${File.separator}$httpTtsFileName") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + val defaultReadConfigs by lazy { + val json = String( + App.INSTANCE.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + val defaultThemeConfigs by lazy { + val json = String( + App.INSTANCE.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/ImageLoader.kt b/app/src/main/java/com/novel/read/help/ImageLoader.kt new file mode 100644 index 0000000..9217df6 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/ImageLoader.kt @@ -0,0 +1,56 @@ +package com.novel.read.help + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.annotation.DrawableRes +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.novel.read.utils.ext.isAbsUrl +import com.novel.read.utils.ext.isContentPath +import java.io.File + +object ImageLoader { + + /** + * 自动判断path类型 + */ + fun load(context: Context, path: String?): RequestBuilder { + return when { + path.isNullOrEmpty() -> Glide.with(context).load(path) + path.isAbsUrl() -> Glide.with(context).load(path) + path.isContentPath() -> Glide.with(context).load(Uri.parse(path)) + else -> try { + Glide.with(context).load(File(path)) + } catch (e: Exception) { + Glide.with(context).load(path) + } + } + } + + fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder { + return Glide.with(context).load(resId) + } + + fun load(context: Context, file: File?): RequestBuilder { + return Glide.with(context).load(file) + } + + fun load(context: Context, uri: Uri?): RequestBuilder { + return Glide.with(context).load(uri) + } + + fun load(context: Context, drawable: Drawable?): RequestBuilder { + return Glide.with(context).load(drawable) + } + + fun load(context: Context, bitmap: Bitmap?): RequestBuilder { + return Glide.with(context).load(bitmap) + } + + fun load(context: Context, bytes: ByteArray?): RequestBuilder { + return Glide.with(context).load(bytes) + } + +} diff --git a/app/src/main/java/com/novel/read/help/IntentDataHelp.kt b/app/src/main/java/com/novel/read/help/IntentDataHelp.kt new file mode 100644 index 0000000..67f4e05 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/IntentDataHelp.kt @@ -0,0 +1,20 @@ +package com.novel.read.help + +object IntentDataHelp { + + private val bigData: MutableMap = mutableMapOf() + + fun putData(data: Any, tag: String = ""): String { + val key = tag + System.currentTimeMillis() + bigData[key] = data + return key + } + + @Suppress("UNCHECKED_CAST") + fun getData(key: String?): T? { + if (key == null) return null + val data = bigData[key] + bigData.remove(key) + return data as? T + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/IntentHelp.kt b/app/src/main/java/com/novel/read/help/IntentHelp.kt new file mode 100644 index 0000000..b4b6bed --- /dev/null +++ b/app/src/main/java/com/novel/read/help/IntentHelp.kt @@ -0,0 +1,61 @@ +package com.novel.read.help + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.novel.read.R +import org.jetbrains.anko.toast + +object IntentHelp { + + + fun toTTSSetting(context: Context) { + //跳转到文字转语音设置界面 + try { + val intent = Intent() + intent.action = "com.android.settings.TTS_SETTINGS" + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } catch (ignored: Exception) { + context.toast(R.string.tip_cannot_jump_setting_page) + } + } + + fun toInstallUnknown(context: Context) { + try { + val intent = Intent() + intent.action = "android.settings.MANAGE_UNKNOWN_APP_SOURCES" + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } catch (ignored: Exception) { + context.toast("无法打开设置") + } + } + + inline fun servicePendingIntent( + context: Context, + action: String, + bundle: Bundle? = null + ): PendingIntent? { + val intent = Intent(context, T::class.java) + intent.action = action + bundle?.let { + intent.putExtras(bundle) + } + return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + inline fun activityPendingIntent( + context: Context, + action: String, + bundle: Bundle? = null + ): PendingIntent? { + val intent = Intent(context, T::class.java) + intent.action = action + bundle?.let { + intent.putExtras(bundle) + } + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/MediaHelp.kt b/app/src/main/java/com/novel/read/help/MediaHelp.kt new file mode 100644 index 0000000..40013cc --- /dev/null +++ b/app/src/main/java/com/novel/read/help/MediaHelp.kt @@ -0,0 +1,82 @@ +package com.novel.read.help + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.media.MediaPlayer +import android.os.Build +import android.support.v4.media.session.PlaybackStateCompat +import com.novel.read.R + +object MediaHelp { + const val MEDIA_SESSION_ACTIONS = (PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + or PlaybackStateCompat.ACTION_REWIND + or PlaybackStateCompat.ACTION_PLAY + or PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_PAUSE + or PlaybackStateCompat.ACTION_STOP + or PlaybackStateCompat.ACTION_FAST_FORWARD + or PlaybackStateCompat.ACTION_SKIP_TO_NEXT + or PlaybackStateCompat.ACTION_SEEK_TO + or PlaybackStateCompat.ACTION_SET_RATING + or PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + or PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + or PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM + or PlaybackStateCompat.ACTION_PLAY_FROM_URI + or PlaybackStateCompat.ACTION_PREPARE + or PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + or PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH + or PlaybackStateCompat.ACTION_PREPARE_FROM_URI + or PlaybackStateCompat.ACTION_SET_REPEAT_MODE + or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE + or PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED) + + fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequest? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mPlaybackAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes(mPlaybackAttributes) + .setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + } else { + null + } + } + + /** + * @return 音频焦点 + */ + fun requestFocus( + audioManager: AudioManager, + listener: AudioManager.OnAudioFocusChangeListener, + focusRequest: AudioFocusRequest? + ): Boolean { + val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + focusRequest?.let { + audioManager.requestAudioFocus(focusRequest) + } ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED + } else { + @Suppress("DEPRECATION") + audioManager.requestAudioFocus( + listener, + AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN + ) + } + return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + } + + fun playSilentSound(mContext: Context) { + kotlin.runCatching { + // Stupid Android 8 "Oreo" hack to make media buttons work + val mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent_sound) + mMediaPlayer.setOnCompletionListener { mMediaPlayer.release() } + mMediaPlayer.start() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/ReadBookConfig.kt b/app/src/main/java/com/novel/read/help/ReadBookConfig.kt new file mode 100644 index 0000000..e19ab19 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/ReadBookConfig.kt @@ -0,0 +1,530 @@ +package com.novel.read.help + +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import androidx.annotation.Keep +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.PreferKey +import com.novel.read.user.VipHelper +import com.novel.read.utils.BitmapUtils +import com.novel.read.utils.FileUtils +import com.novel.read.utils.ext.* +import com.novel.read.help.coroutine.Coroutine +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import java.io.File + +/** + * 阅读界面配置 + */ +@Keep +object ReadBookConfig { + const val configFileName = "readConfig.json" + const val shareConfigFileName = "shareReadConfig.json" + val configFilePath = FileUtils.getPath(App.INSTANCE.filesDir, configFileName) + val shareConfigFilePath = FileUtils.getPath(App.INSTANCE.filesDir, shareConfigFileName) + val configList: ArrayList = arrayListOf() + lateinit var shareConfig: Config + var durConfig + get() = getConfig(styleSelect) + set(value) { + configList[styleSelect] = value + if (shareLayout) { + shareConfig = value + } + upBg() + } + + var bg: Drawable? = null + var bgMeanColor: Int = 0 + val textColor: Int get() = durConfig.curTextColor() + + init { + initConfigs() + initShareConfig() + } + + @Synchronized + fun getConfig(index: Int): Config { + if (configList.size < 5) { + resetAll() + } + return configList.getOrNull(index) ?: configList[0] + } + + fun initConfigs() { + val configFile = File(configFilePath) + var configs: List? = null + if (configFile.exists()) { + try { + val json = configFile.readText() + configs = GSON.fromJsonArray(json) + } catch (e: Exception) { + e.printStackTrace() + } + } + (configs ?: DefaultData.defaultReadConfigs).let { + configList.clear() + configList.addAll(it) + } + } + + fun initShareConfig() { + val configFile = File(shareConfigFilePath) + var c: Config? = null + if (configFile.exists()) { + try { + val json = configFile.readText() + c = GSON.fromJsonObject(json) + } catch (e: Exception) { + e.printStackTrace() + } + } + shareConfig = c ?: configList.getOrNull(5) ?: Config() + } + + fun upBg() { + val resources = App.INSTANCE.resources + val dm = resources.displayMetrics + val width = dm.widthPixels + val height = dm.heightPixels + bg = durConfig.curBgDrawable(width, height).apply { + if (this is BitmapDrawable) { + bgMeanColor = BitmapUtils.getMeanColor(bitmap) + } else if (this is ColorDrawable) { + bgMeanColor = color + } + } + } + + fun save() { + Coroutine.async { + synchronized(this) { + GSON.toJson(configList).let { + FileUtils.deleteFile(configFilePath) + FileUtils.createFileIfNotExist(configFilePath).writeText(it) + } + GSON.toJson(shareConfig).let { + FileUtils.deleteFile(shareConfigFilePath) + FileUtils.createFileIfNotExist(shareConfigFilePath).writeText(it) + } + } + } + } + + fun deleteDur(): Boolean { + if (configList.size > 5) { + configList.removeAt(styleSelect) + if (styleSelect > 0) { + styleSelect -= 1 + } + upBg() + return true + } + return false + } + + private fun resetAll() { + DefaultData.defaultReadConfigs.let { + configList.clear() + configList.addAll(it) + save() + } + } + + //配置写入读取 + var autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46) + set(value) { + field = value + App.INSTANCE.putPrefInt(PreferKey.autoReadSpeed, value) + } + var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) + set(value) { + field = value + if (App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) != value) { + App.INSTANCE.putPrefInt(PreferKey.readStyleSelect, value) + } + } + var shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout, true) + set(value) { + field = value + if (App.INSTANCE.getPrefBoolean(PreferKey.shareLayout) != value) { + App.INSTANCE.putPrefBoolean(PreferKey.shareLayout, value) + } + } + val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true) + val clickAllNext get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false) + val textFullJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textFullJustify, true) + val textBottomJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textBottomJustify, true) + var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) + var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) + + val config get() = if (shareLayout) shareConfig else durConfig + + var pageAnim: Int + get() = config.curPageAnim() + set(value) { + config.setCurPageAnim(value) + } + + var textFont: String + get() = config.textFont + set(value) { + config.textFont = value + } + + var textBold: Int + get() = config.textBold + set(value) { + config.textBold = value + } + + var textSize: Int + get() = config.textSize + set(value) { + config.textSize = value + } + + var letterSpacing: Float + get() = config.letterSpacing + set(value) { + config.letterSpacing = value + } + + var lineSpacingExtra: Int + get() = config.lineSpacingExtra + set(value) { + config.lineSpacingExtra = value + } + + var paragraphSpacing: Int + get() = config.paragraphSpacing + set(value) { + config.paragraphSpacing = value + } + + var titleMode: Int + get() = config.titleMode + set(value) { + config.titleMode = value + } + var titleSize: Int + get() = config.titleSize + set(value) { + config.titleSize = value + } + + var titleTopSpacing: Int + get() = config.titleTopSpacing + set(value) { + config.titleTopSpacing = value + } + + var titleBottomSpacing: Int + get() = config.titleBottomSpacing + set(value) { + config.titleBottomSpacing = value + } + + var paragraphIndent: String + get() = config.paragraphIndent + set(value) { + config.paragraphIndent = value + } + + var paddingBottom: Int +// get() = config.paddingBottom + get() = if (VipHelper.showAd()) { + 10 + } else { + 20 + } + set(value) { + config.paddingBottom = value + } + + var paddingLeft: Int + get() = config.paddingLeft + set(value) { + config.paddingLeft = value + } + + var paddingRight: Int + get() = config.paddingRight + set(value) { + config.paddingRight = value + } + + var paddingTop: Int + get() = config.paddingTop + set(value) { + config.paddingTop = value + } + + var headerPaddingBottom: Int + get() = config.headerPaddingBottom + set(value) { + config.headerPaddingBottom = value + } + + var headerPaddingLeft: Int + get() = config.headerPaddingLeft + set(value) { + config.headerPaddingLeft = value + } + + var headerPaddingRight: Int + get() = config.headerPaddingRight + set(value) { + config.headerPaddingRight = value + } + + var headerPaddingTop: Int + get() = config.headerPaddingTop + set(value) { + config.headerPaddingTop = value + } + + var footerPaddingBottom: Int + // get() = config.footerPaddingBottom + get() = if (VipHelper.showAd()) { + 60 + } else { + 10 + } + set(value) { + config.footerPaddingBottom = value + } + + var footerPaddingLeft: Int + get() = config.footerPaddingLeft + set(value) { + config.footerPaddingLeft = value + } + + var footerPaddingRight: Int + get() = config.footerPaddingRight + set(value) { + config.footerPaddingRight = value + } + + var footerPaddingTop: Int + get() = config.footerPaddingTop + set(value) { + config.footerPaddingTop = value + } + + var showHeaderLine: Boolean + get() = config.showHeaderLine + set(value) { + config.showHeaderLine = value + } + + var showFooterLine: Boolean + get() = config.showFooterLine + set(value) { + config.showFooterLine = value + } + + fun getExportConfig(): Config { + val exportConfig = GSON.fromJsonObject(GSON.toJson(durConfig))!! + if (shareLayout) { + exportConfig.textFont = shareConfig.textFont + exportConfig.textBold = shareConfig.textBold + exportConfig.textSize = shareConfig.textSize + exportConfig.letterSpacing = shareConfig.letterSpacing + exportConfig.lineSpacingExtra = shareConfig.lineSpacingExtra + exportConfig.paragraphSpacing = shareConfig.paragraphSpacing + exportConfig.titleMode = shareConfig.titleMode + exportConfig.titleSize = shareConfig.titleSize + exportConfig.titleTopSpacing = shareConfig.titleTopSpacing + exportConfig.titleBottomSpacing = shareConfig.titleBottomSpacing + exportConfig.paddingBottom = shareConfig.paddingBottom + exportConfig.paddingLeft = shareConfig.paddingLeft + exportConfig.paddingRight = shareConfig.paddingRight + exportConfig.paddingTop = shareConfig.paddingTop + exportConfig.headerPaddingBottom = shareConfig.headerPaddingBottom + exportConfig.headerPaddingLeft = shareConfig.headerPaddingLeft + exportConfig.headerPaddingRight = shareConfig.headerPaddingRight + exportConfig.headerPaddingTop = shareConfig.headerPaddingTop + exportConfig.footerPaddingBottom = shareConfig.footerPaddingBottom + exportConfig.footerPaddingLeft = shareConfig.footerPaddingLeft + exportConfig.footerPaddingRight = shareConfig.footerPaddingRight + exportConfig.footerPaddingTop = shareConfig.footerPaddingTop + exportConfig.showHeaderLine = shareConfig.showHeaderLine + exportConfig.showFooterLine = shareConfig.showFooterLine + exportConfig.tipHeaderLeft = shareConfig.tipHeaderLeft + exportConfig.tipHeaderMiddle = shareConfig.tipHeaderMiddle + exportConfig.tipHeaderRight = shareConfig.tipHeaderRight + exportConfig.tipFooterLeft = shareConfig.tipFooterLeft + exportConfig.tipFooterMiddle = shareConfig.tipFooterMiddle + exportConfig.tipFooterRight = shareConfig.tipFooterRight + exportConfig.hideHeader = shareConfig.hideHeader + exportConfig.hideFooter = shareConfig.hideFooter + } + return exportConfig + } + + @Keep + class Config( + var name: String = "", + var bgStr: String = "#EFEFF7",//白天背景 + var bgStrNight: String = "#000000",//夜间背景 + var bgStrEInk: String = "#FFFFFF", + var bgType: Int = 0,//白天背景类型 0:颜色, 1:assets图片, 2其它图片 + var bgTypeNight: Int = 0,//夜间背景类型 + var bgTypeEInk: Int = 0, + private var darkStatusIcon: Boolean = true,//白天是否暗色状态栏 + private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 + private var darkStatusIconEInk: Boolean = true, + private var textColor: String = "#383429",//白天文字颜色 + private var textColorNight: String = "#ADADAD",//夜间文字颜色 + private var textColorEInk: String = "#000000", + private var pageAnim: Int = 0, + private var pageAnimEInk: Int = 3, + var textFont: String = "",//字体 + var textBold: Int = 0,//是否粗体字 0:正常, 1:粗体, 2:细体 + var textSize: Int = 20,//文字大小 + var letterSpacing: Float = 0.1f,//字间距 + var lineSpacingExtra: Int = 13,//行间距 + var paragraphSpacing: Int = 4,//段距 + var titleMode: Int = 0,//标题居中 1 居中 + var titleSize: Int = 5, + var titleTopSpacing: Int = 12, + var titleBottomSpacing: Int = 0, + var paragraphIndent: String = "  ",//段落缩进 + var paddingBottom: Int = 6, + var paddingLeft: Int = 16, + var paddingRight: Int = 16, + var paddingTop: Int = 10, + var headerPaddingBottom: Int = 0, + var headerPaddingLeft: Int = 16, + var headerPaddingRight: Int = 16, + var headerPaddingTop: Int = 0, + var footerPaddingBottom: Int = 60, + var footerPaddingLeft: Int = 16, + var footerPaddingRight: Int = 16, + var footerPaddingTop: Int = 6, + var showHeaderLine: Boolean = false, + var showFooterLine: Boolean = false, + var tipHeaderLeft: Int = ReadTipConfig.time, + var tipHeaderMiddle: Int = ReadTipConfig.none, + var tipHeaderRight: Int = ReadTipConfig.battery, + var tipFooterLeft: Int = ReadTipConfig.chapterTitle, + var tipFooterMiddle: Int = ReadTipConfig.none, + var tipFooterRight: Int = ReadTipConfig.pageAndTotal, + var hideHeader: Boolean = true, + var hideFooter: Boolean = false + ) { + + fun setCurTextColor(color: Int) { + when { + AppConfig.isEInkMode -> textColorEInk = "#${color.hexString}" + AppConfig.isNightTheme -> textColorNight = "#${color.hexString}" + else -> textColor = "#${color.hexString}" + } + ChapterProvider.upStyle() + } + + fun curTextColor(): Int { + return when { + AppConfig.isEInkMode -> Color.parseColor(textColorEInk) + AppConfig.isNightTheme -> Color.parseColor(textColorNight) + else -> Color.parseColor(textColor) + } + } + + fun setCurStatusIconDark(isDark: Boolean) { + when { + AppConfig.isEInkMode -> darkStatusIconEInk = isDark + AppConfig.isNightTheme -> darkStatusIconNight = isDark + else -> darkStatusIcon = isDark + } + } + + fun curStatusIconDark(): Boolean { + return when { + AppConfig.isEInkMode -> darkStatusIconEInk + AppConfig.isNightTheme -> darkStatusIconNight + else -> darkStatusIcon + } + } + + fun setCurPageAnim(anim: Int) { + when { + AppConfig.isEInkMode -> pageAnimEInk = anim + else -> pageAnim = anim + } + } + + fun curPageAnim(): Int { + return when { + AppConfig.isEInkMode -> pageAnimEInk + else -> pageAnim + } + } + + fun setCurBg(bgType: Int, bg: String) { + when { + AppConfig.isEInkMode -> { + bgTypeEInk = bgType + bgStrEInk = bg + } + AppConfig.isNightTheme -> { + bgTypeNight = bgType + bgStrNight = bg + } + else -> { + this.bgType = bgType + bgStr = bg + } + } + } + + fun curBgStr(): String { + return when { + AppConfig.isEInkMode -> bgStrEInk + AppConfig.isNightTheme -> bgStrNight + else -> bgStr + } + } + + fun curBgType(): Int { + return when { + AppConfig.isEInkMode -> bgTypeEInk + AppConfig.isNightTheme -> bgTypeNight + else -> bgType + } + } + + fun curBgDrawable(width: Int, height: Int): Drawable { + var bgDrawable: Drawable? = null + val resources = App.INSTANCE.resources + try { + bgDrawable = when (curBgType()) { + 0 -> ColorDrawable(Color.parseColor(curBgStr())) + 1 -> { + BitmapDrawable( + resources, + BitmapUtils.decodeAssetsBitmap( + App.INSTANCE, + "bg" + File.separator + curBgStr(), + width, + height + ) + ) + } + else -> BitmapDrawable( + resources, + BitmapUtils.decodeBitmap(curBgStr(), width, height) + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + return bgDrawable ?: ColorDrawable(App.INSTANCE.getCompatColor(R.color.background)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/ReadTipConfig.kt b/app/src/main/java/com/novel/read/help/ReadTipConfig.kt new file mode 100644 index 0000000..7504a3c --- /dev/null +++ b/app/src/main/java/com/novel/read/help/ReadTipConfig.kt @@ -0,0 +1,71 @@ +package com.novel.read.help + +import com.novel.read.App +import com.novel.read.R + +object ReadTipConfig { + val tipArray: Array = App.INSTANCE.resources.getStringArray(R.array.read_tip) + const val none = 0 + const val chapterTitle = 1 + const val time = 2 + const val battery = 3 + const val page = 4 + const val totalProgress = 5 + const val pageAndTotal = 6 + const val bookName = 7 + + val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] } + val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] } + val tipHeaderRightStr: String get() = tipArray.getOrElse(tipHeaderRight) { tipArray[none] } + val tipFooterLeftStr: String get() = tipArray.getOrElse(tipFooterLeft) { tipArray[none] } + val tipFooterMiddleStr: String get() = tipArray.getOrElse(tipFooterMiddle) { tipArray[none] } + val tipFooterRightStr: String get() = tipArray.getOrElse(tipFooterRight) { tipArray[none] } + + var tipHeaderLeft: Int + get() = ReadBookConfig.config.tipHeaderLeft + set(value) { + ReadBookConfig.config.tipHeaderLeft = value + } + + var tipHeaderMiddle: Int + get() = ReadBookConfig.config.tipHeaderMiddle + set(value) { + ReadBookConfig.config.tipHeaderMiddle = value + } + + var tipHeaderRight: Int + get() = ReadBookConfig.config.tipHeaderRight + set(value) { + ReadBookConfig.config.tipHeaderRight = value + } + + var tipFooterLeft: Int + get() = ReadBookConfig.config.tipFooterLeft + set(value) { + ReadBookConfig.config.tipFooterLeft = value + } + + var tipFooterMiddle: Int + get() = ReadBookConfig.config.tipFooterMiddle + set(value) { + ReadBookConfig.config.tipFooterMiddle = value + } + + var tipFooterRight: Int + get() = ReadBookConfig.config.tipFooterRight + set(value) { + ReadBookConfig.config.tipFooterRight = value + } + + var hideHeader: Boolean + get() = ReadBookConfig.config.hideHeader + set(value) { + ReadBookConfig.config.hideHeader = value + } + + var hideFooter: Boolean + get() = ReadBookConfig.config.hideFooter + set(value) { + ReadBookConfig.config.hideFooter = value + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/ThemeConfig.kt b/app/src/main/java/com/novel/read/help/ThemeConfig.kt new file mode 100644 index 0000000..30a37f6 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/ThemeConfig.kt @@ -0,0 +1,212 @@ +package com.novel.read.help + +import android.content.Context +import android.graphics.Color +import androidx.annotation.Keep +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.EventBus +import com.novel.read.constant.PreferKey +import com.novel.read.lib.ThemeStore +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.FileUtils +import com.novel.read.utils.ext.* +import java.io.File + +object ThemeConfig { + const val configFileName = "themeConfig.json" + val configFilePath = FileUtils.getPath(App.INSTANCE.filesDir, configFileName) + + val configList: ArrayList by lazy { + val cList = getConfigs() ?: DefaultData.defaultThemeConfigs + ArrayList(cList) + } + + fun upConfig() { + getConfigs()?.let { + it.forEach { config -> + addConfig(config) + } + } + } + + fun save() { + val json = GSON.toJson(configList) + FileUtils.deleteFile(configFilePath) + FileUtils.createFileIfNotExist(configFilePath).writeText(json) + } + + fun delConfig(index: Int) { + configList.removeAt(index) + save() + } + + fun addConfig(json: String): Boolean { + GSON.fromJsonObject(json.trim { it < ' ' })?.let { + addConfig(it) + return true + } + return false + } + + private fun addConfig(newConfig: Config) { + configList.forEachIndexed { index, config -> + if (newConfig.themeName == config.themeName) { + configList[index] = newConfig + return + } + } + configList.add(newConfig) + save() + } + + private fun getConfigs(): List? { + val configFile = File(configFilePath) + if (configFile.exists()) { + try { + val json = configFile.readText() + return GSON.fromJsonArray(json) + } catch (e: Exception) { + e.printStackTrace() + } + } + return null + } + + fun applyConfig(context: Context, config: Config) { + val primary = Color.parseColor(config.primaryColor) + val accent = Color.parseColor(config.accentColor) + val background = Color.parseColor(config.backgroundColor) + val bBackground = Color.parseColor(config.bottomBackground) + if (config.isNightTheme) { + context.putPrefInt(PreferKey.cNPrimary, primary) + context.putPrefInt(PreferKey.cNAccent, accent) + context.putPrefInt(PreferKey.cNBackground, background) + context.putPrefInt(PreferKey.cNBBackground, bBackground) + } else { + context.putPrefInt(PreferKey.cPrimary, primary) + context.putPrefInt(PreferKey.cAccent, accent) + context.putPrefInt(PreferKey.cBackground, background) + context.putPrefInt(PreferKey.cBBackground, bBackground) + } + AppConfig.isNightTheme = config.isNightTheme + App.INSTANCE.applyDayNight() + postEvent(EventBus.RECREATE, "") + } + + fun saveDayTheme(context: Context, name: String) { + val primary = + context.getPrefInt(PreferKey.cPrimary, context.getCompatColor(R.color.md_brown_500)) + val accent = + context.getPrefInt(PreferKey.cAccent, context.getCompatColor(R.color.md_red_600)) + val background = + context.getPrefInt(PreferKey.cBackground, context.getCompatColor(R.color.md_grey_100)) + val bBackground = + context.getPrefInt(PreferKey.cBBackground, context.getCompatColor(R.color.md_grey_200)) + val config = Config( + themeName = name, + isNightTheme = false, + primaryColor = "#${primary.hexString}", + accentColor = "#${accent.hexString}", + backgroundColor = "#${background.hexString}", + bottomBackground = "#${bBackground.hexString}" + ) + addConfig(config) + } + + fun saveNightTheme(context: Context, name: String) { + val primary = + context.getPrefInt( + PreferKey.cNPrimary, + context.getCompatColor(R.color.md_blue_grey_600) + ) + val accent = + context.getPrefInt( + PreferKey.cNAccent, + context.getCompatColor(R.color.md_deep_orange_800) + ) + val background = + context.getPrefInt(PreferKey.cNBackground, context.getCompatColor(R.color.md_grey_900)) + val bBackground = + context.getPrefInt(PreferKey.cNBBackground, context.getCompatColor(R.color.md_grey_850)) + val config = Config( + themeName = name, + isNightTheme = true, + primaryColor = "#${primary.hexString}", + accentColor = "#${accent.hexString}", + backgroundColor = "#${background.hexString}", + bottomBackground = "#${bBackground.hexString}" + ) + addConfig(config) + } + + /** + * 更新主题 + */ + fun applyTheme(context: Context) = with(context) { + when { + AppConfig.isEInkMode -> { + ThemeStore.editTheme(this) + .coloredNavigationBar(true) + .primaryColor(Color.WHITE) + .accentColor(Color.BLACK) + .backgroundColor(Color.WHITE) + .bottomBackground(Color.WHITE) + .apply() + } + AppConfig.isNightTheme -> { + val primary = + getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_grey_900)) + val accent = + getPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_amber_800)) + var background = + getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900)) + if (ColorUtils.isColorLight(background)) { + background = getCompatColor(R.color.md_grey_900) + putPrefInt(PreferKey.cNBackground, background) + } + val bBackground = + getPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_900)) + ThemeStore.editTheme(this) + .coloredNavigationBar(true) + .primaryColor(ColorUtils.withAlpha(primary, 1f)) + .accentColor(ColorUtils.withAlpha(accent, 1f)) + .backgroundColor(ColorUtils.withAlpha(background, 1f)) + .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) + .apply() + } + else -> { + val primary = + getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_grey_50)) + val accent = + getPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_amber_800)) + var background = + getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_50)) + if (!ColorUtils.isColorLight(background)) { + background = getCompatColor(R.color.md_grey_50) + putPrefInt(PreferKey.cBackground, background) + } + val bBackground = + getPrefInt(PreferKey.cBBackground, getCompatColor(R.color.white)) + ThemeStore.editTheme(this) + .coloredNavigationBar(true) + .primaryColor(ColorUtils.withAlpha(primary, 1f)) + .accentColor(ColorUtils.withAlpha(accent, 1f)) + .backgroundColor(ColorUtils.withAlpha(background, 1f)) + .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) + .apply() + } + } + } + + @Keep + class Config( + var themeName: String, + var isNightTheme: Boolean, + var primaryColor: String, + var accentColor: String, + var backgroundColor: String, + var bottomBackground: String + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/TintHelper.kt b/app/src/main/java/com/novel/read/help/TintHelper.kt new file mode 100644 index 0000000..d20c9d9 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/TintHelper.kt @@ -0,0 +1,488 @@ +package com.novel.read.help + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.graphics.drawable.RippleDrawable +import android.os.Build +import android.view.View +import android.widget.* +import androidx.annotation.CheckResult +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.novel.read.R +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ViewUtils + +/** + * @author afollestad, plusCubed + */ +@Suppress("MemberVisibilityCanBePrivate") +object TintHelper { + + @SuppressLint("PrivateResource") + @ColorInt + private fun getDefaultRippleColor(context: Context, useDarkRipple: Boolean): Int { + // Light ripple is actually translucent black, and vice versa + return ContextCompat.getColor( + context, if (useDarkRipple) + R.color.ripple_material_light + else + R.color.ripple_material_dark + ) + } + + private fun getDisabledColorStateList( + @ColorInt normal: Int, + @ColorInt disabled: Int + ): ColorStateList { + return ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled) + ), intArrayOf(disabled, normal) + ) + } + + fun setTintSelector(view: View, @ColorInt color: Int, darker: Boolean, useDarkTheme: Boolean) { + val isColorLight = ColorUtils.isColorLight(color) + val disabled = ContextCompat.getColor( + view.context, + if (useDarkTheme) R.color.ate_button_disabled_dark else R.color.ate_button_disabled_light + ) + val pressed = ColorUtils.shiftColor(color, if (darker) 0.9f else 1.1f) + val activated = ColorUtils.shiftColor(color, if (darker) 1.1f else 0.9f) + val rippleColor = getDefaultRippleColor(view.context, isColorLight) + val textColor = ContextCompat.getColor( + view.context, + if (isColorLight) R.color.ate_primary_text_light else R.color.ate_primary_text_dark + ) + + val sl: ColorStateList + when (view) { + is Button -> { + sl = getDisabledColorStateList(color, disabled) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && view.getBackground() is RippleDrawable) { + val rd = view.getBackground() as RippleDrawable + rd.setColor(ColorStateList.valueOf(rippleColor)) + } + + // Disabled text color state for buttons, may get overridden later by ATE tags + view.setTextColor( + getDisabledColorStateList( + textColor, + ContextCompat.getColor( + view.getContext(), + if (useDarkTheme) R.color.ate_button_text_disabled_dark else R.color.ate_button_text_disabled_light + ) + ) + ) + } + is FloatingActionButton -> { + // FloatingActionButton doesn't support disabled state? + sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_pressed) + ), intArrayOf(color, pressed) + ) + + view.rippleColor = rippleColor + view.backgroundTintList = sl + if (view.drawable != null) + view.setImageDrawable(createTintedDrawable(view.drawable, textColor)) + return + } + else -> { + sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf(disabled, color, pressed, activated, activated) + ) + } + } + + var drawable: Drawable? = view.background + if (drawable != null) { + drawable = createTintedDrawable(drawable, sl) + ViewUtils.setBackgroundCompat(view, drawable) + } + + if (view is TextView && view !is Button) { + view.setTextColor( + getDisabledColorStateList( + textColor, + ContextCompat.getColor( + view.getContext(), + if (isColorLight) R.color.ate_text_disabled_light else R.color.ate_text_disabled_dark + ) + ) + ) + } + } + + fun setTintAuto( + view: View, @ColorInt color: Int, + isBackground: Boolean, isDark: Boolean + ) { + var isBg = isBackground + if (!isBg) { + when (view) { + is RadioButton -> setTint(view, color, isDark) + is SeekBar -> setTint(view, color, isDark) + is ProgressBar -> setTint(view, color) + is AppCompatEditText -> setTint(view, color, isDark) + is CheckBox -> setTint(view, color, isDark) + is ImageView -> setTint(view, color) + is Switch -> setTint(view, color, isDark) + is SwitchCompat -> setTint(view, color, isDark) + is SearchView -> { + val iconIdS = + intArrayOf( + androidx.appcompat.R.id.search_button, + androidx.appcompat.R.id.search_close_btn, + androidx.appcompat.R.id.search_go_btn, + androidx.appcompat.R.id.search_voice_btn, + androidx.appcompat.R.id.search_mag_icon + ) + for (iconId in iconIdS) { + val icon = view.findViewById(iconId) + if (icon != null) { + setTint(icon, color) + } + } + } + else -> isBg = true + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && + !isBg && view.background is RippleDrawable + ) { + // Ripples for the above views (e.g. when you tap and hold a switch or checkbox) + val rd = view.background as RippleDrawable + @SuppressLint("PrivateResource") val unchecked = ContextCompat.getColor( + view.context, + if (isDark) R.color.ripple_material_dark else R.color.ripple_material_light + ) + val checked = ColorUtils.adjustAlpha(color, 0.4f) + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_activated, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_activated), + intArrayOf(android.R.attr.state_checked) + ), + intArrayOf(unchecked, checked, checked) + ) + rd.setColor(sl) + } + } + if (isBg) { + // Need to tint the isBackground of a view + if (view is FloatingActionButton || view is Button) { + setTintSelector(view, color, false, isDark) + } else if (view.background != null) { + var drawable: Drawable? = view.background + if (drawable != null) { + drawable = createTintedDrawable(drawable, color) + ViewUtils.setBackgroundCompat(view, drawable) + } + } + } + } + + @SuppressLint("PrivateResource") + fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) { + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), intArrayOf( + // Radio button includes own alpha for disabled state + ColorUtils.stripAlpha( + ContextCompat.getColor( + radioButton.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ) + ), + ContextCompat.getColor( + radioButton.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + radioButton.buttonTintList = sl + } else { + radioButton.buttonDrawable = createTintedDrawable( + ContextCompat.getDrawable(radioButton.context, R.drawable.abc_btn_radio_material), + sl + ) + } + } + + fun setTint(seekBar: SeekBar, @ColorInt color: Int, useDarker: Boolean) { + val s1 = getDisabledColorStateList( + color, + ContextCompat.getColor( + seekBar.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ) + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + seekBar.thumbTintList = s1 + seekBar.progressTintList = s1 + } else { + val progressDrawable = createTintedDrawable(seekBar.progressDrawable, s1) + seekBar.progressDrawable = progressDrawable + val thumbDrawable = createTintedDrawable(seekBar.thumb, s1) + seekBar.thumb = thumbDrawable + } + } + + @JvmOverloads + fun setTint( + progressBar: ProgressBar, @ColorInt color: Int, + skipIndeterminate: Boolean = false + ) { + val sl = ColorStateList.valueOf(color) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + progressBar.progressTintList = sl + progressBar.secondaryProgressTintList = sl + if (!skipIndeterminate) + progressBar.indeterminateTintList = sl + } else { + if (!skipIndeterminate && progressBar.indeterminateDrawable != null) + progressBar.indeterminateDrawable.setTint(color) + if (progressBar.progressDrawable != null) + progressBar.progressDrawable.setTint(color) + } + } + + + @SuppressLint("RestrictedApi") + fun setTint(editText: AppCompatEditText, @ColorInt color: Int, useDarker: Boolean) { + val editTextColorStateList = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf( + android.R.attr.state_enabled, + -android.R.attr.state_pressed, + -android.R.attr.state_focused + ), + intArrayOf() + ), + intArrayOf( + ContextCompat.getColor( + editText.context, + if (useDarker) R.color.ate_text_disabled_dark else R.color.ate_text_disabled_light + ), + ContextCompat.getColor( + editText.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + editText.supportBackgroundTintList = editTextColorStateList + setCursorTint(editText, color) + } + + @SuppressLint("PrivateResource") + fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) { + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf( + ContextCompat.getColor( + box.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ), + ContextCompat.getColor( + box.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + box.buttonTintList = sl + } else { + val drawable = + createTintedDrawable( + ContextCompat.getDrawable( + box.context, + R.drawable.abc_btn_check_material + ), sl + ) + box.buttonDrawable = drawable + } + } + + fun setTint(image: ImageView, @ColorInt color: Int) { + image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + + private fun modifySwitchDrawable( + context: Context, + from: Drawable, + @ColorInt tint: Int, + thumb: Boolean, + compatSwitch: Boolean, + useDarker: Boolean + ): Drawable? { + var tint1 = tint + if (useDarker) { + tint1 = ColorUtils.shiftColor(tint1, 1.1f) + } + tint1 = ColorUtils.adjustAlpha(tint1, if (compatSwitch && !thumb) 0.5f else 1.0f) + val disabled: Int + var normal: Int + if (thumb) { + disabled = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_thumb_disabled_dark else R.color.ate_switch_thumb_disabled_light + ) + normal = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_thumb_normal_dark else R.color.ate_switch_thumb_normal_light + ) + } else { + disabled = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_track_disabled_dark else R.color.ate_switch_track_disabled_light + ) + normal = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_track_normal_dark else R.color.ate_switch_track_normal_light + ) + } + + // Stock switch includes its own alpha + if (!compatSwitch) { + normal = ColorUtils.stripAlpha(normal) + } + + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf( + android.R.attr.state_enabled, + -android.R.attr.state_activated, + -android.R.attr.state_checked + ), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf(disabled, normal, tint1, tint1) + ) + return createTintedDrawable(from, sl) + } + + fun setTint( + @SuppressLint("UseSwitchCompatOrMaterialCode") switchView: Switch, + @ColorInt color: Int, + useDarker: Boolean + ) { + if (switchView.trackDrawable != null) { + switchView.trackDrawable = modifySwitchDrawable( + switchView.context, + switchView.trackDrawable, + color, + thumb = false, + compatSwitch = false, + useDarker = useDarker + ) + } + if (switchView.thumbDrawable != null) { + switchView.thumbDrawable = modifySwitchDrawable( + switchView.context, + switchView.thumbDrawable, + color, + thumb = true, + compatSwitch = false, + useDarker = useDarker + ) + } + } + + fun setTint(switchView: SwitchCompat, @ColorInt color: Int, useDarker: Boolean) { + if (switchView.trackDrawable != null) { + switchView.trackDrawable = modifySwitchDrawable( + switchView.context, + switchView.trackDrawable, + color, + thumb = false, + compatSwitch = true, + useDarker = useDarker + ) + } + if (switchView.thumbDrawable != null) { + switchView.thumbDrawable = modifySwitchDrawable( + switchView.context, + switchView.thumbDrawable, + color, + thumb = true, + compatSwitch = true, + useDarker = useDarker + ) + } + } + + // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise. + @CheckResult + fun createTintedDrawable(drawable: Drawable?, @ColorInt color: Int): Drawable? { + var drawable1: Drawable? = drawable ?: return null + drawable1 = DrawableCompat.wrap(drawable1!!.mutate()) + DrawableCompat.setTintMode(drawable1!!, PorterDuff.Mode.SRC_IN) + DrawableCompat.setTint(drawable1, color) + return drawable1 + } + + // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise. + @CheckResult + fun createTintedDrawable(drawable: Drawable?, sl: ColorStateList): Drawable? { + var drawable1: Drawable? = drawable ?: return null + drawable1 = DrawableCompat.wrap(drawable1!!.mutate()) + DrawableCompat.setTintList(drawable1!!, sl) + return drawable1 + } + + fun setCursorTint(editText: EditText, @ColorInt color: Int) { + try { + val fCursorDrawableRes = TextView::class.java.getDeclaredField("mCursorDrawableRes") + fCursorDrawableRes.isAccessible = true + val mCursorDrawableRes = fCursorDrawableRes.getInt(editText) + val fEditor = TextView::class.java.getDeclaredField("mEditor") + fEditor.isAccessible = true + val editor = fEditor.get(editText) + val clazz = editor.javaClass + val fCursorDrawable = clazz.getDeclaredField("mCursorDrawable") + fCursorDrawable.isAccessible = true + val drawables = arrayOfNulls(2) + drawables[0] = ContextCompat.getDrawable(editText.context, mCursorDrawableRes) + drawables[0] = createTintedDrawable(drawables[0], color) + drawables[1] = ContextCompat.getDrawable(editText.context, mCursorDrawableRes) + drawables[1] = createTintedDrawable(drawables[1], color) + fCursorDrawable.set(editor, drawables) + } catch (ignored: Exception) { + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/coroutine/CompositeCoroutine.kt b/app/src/main/java/com/novel/read/help/coroutine/CompositeCoroutine.kt new file mode 100644 index 0000000..fbdf434 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/coroutine/CompositeCoroutine.kt @@ -0,0 +1,83 @@ +package com.novel.read.help.coroutine + +class CompositeCoroutine : CoroutineContainer { + + private var resources: HashSet>? = null + + val size: Int + get() = resources?.size ?: 0 + + val isEmpty: Boolean + get() = size == 0 + + constructor() + + constructor(vararg coroutines: Coroutine<*>) { + this.resources = hashSetOf(*coroutines) + } + + constructor(coroutines: Iterable>) { + this.resources = hashSetOf() + for (d in coroutines) { + this.resources?.add(d) + } + } + + override fun add(coroutine: Coroutine<*>): Boolean { + synchronized(this) { + var set: HashSet>? = resources + if (resources == null) { + set = hashSetOf() + resources = set + } + return set!!.add(coroutine) + } + } + + override fun addAll(vararg coroutines: Coroutine<*>): Boolean { + synchronized(this) { + var set: HashSet>? = resources + if (resources == null) { + set = hashSetOf() + resources = set + } + for (coroutine in coroutines) { + val add = set!!.add(coroutine) + if (!add) { + return false + } + } + } + return true + } + + override fun remove(coroutine: Coroutine<*>): Boolean { + if (delete(coroutine)) { + coroutine.cancel() + return true + } + return false + } + + override fun delete(coroutine: Coroutine<*>): Boolean { + synchronized(this) { + val set = resources + if (set == null || !set.remove(coroutine)) { + return false + } + } + return true + } + + override fun clear() { + val set: HashSet>? + synchronized(this) { + set = resources + resources = null + } + + set?.forEachIndexed { _, coroutine -> + coroutine.cancel() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/help/coroutine/Coroutine.kt b/app/src/main/java/com/novel/read/help/coroutine/Coroutine.kt new file mode 100644 index 0000000..f96c5e2 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/coroutine/Coroutine.kt @@ -0,0 +1,214 @@ +package com.novel.read.help.coroutine + +import com.novel.read.BuildConfig +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext + + +class Coroutine( + val scope: CoroutineScope, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T +) { + + companion object { + + val DEFAULT = MainScope() + + fun async( + scope: CoroutineScope = DEFAULT, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine(scope, context, block) + } + + } + + private val job: Job + + private var start: VoidCallback? = null + private var success: Callback? = null + private var error: Callback? = null + private var finally: VoidCallback? = null + private var cancel: VoidCallback? = null + + private var timeMillis: Long? = null + private var errorReturn: Result? = null + + val isCancelled: Boolean + get() = job.isCancelled + + val isActive: Boolean + get() = job.isActive + + val isCompleted: Boolean + get() = job.isCompleted + + init { + this.job = executeInternal(context, block) + } + + fun timeout(timeMillis: () -> Long): Coroutine { + this.timeMillis = timeMillis() + return this@Coroutine + } + + fun timeout(timeMillis: Long): Coroutine { + this.timeMillis = timeMillis + return this@Coroutine + } + + fun onErrorReturn(value: () -> T?): Coroutine { + this.errorReturn = Result(value()) + return this@Coroutine + } + + fun onErrorReturn(value: T?): Coroutine { + this.errorReturn = Result(value) + return this@Coroutine + } + + fun onStart( + context: CoroutineContext? = null, + block: (suspend CoroutineScope.() -> Unit) + ): Coroutine { + this.start = VoidCallback(context, block) + return this@Coroutine + } + + fun onSuccess( + context: CoroutineContext? = null, + block: suspend CoroutineScope.(T) -> Unit + ): Coroutine { + this.success = Callback(context, block) + return this@Coroutine + } + + fun onError( + context: CoroutineContext? = null, + block: suspend CoroutineScope.(Throwable) -> Unit + ): Coroutine { + this.error = Callback(context, block) + return this@Coroutine + } + + fun onFinally( + context: CoroutineContext? = null, + block: suspend CoroutineScope.() -> Unit + ): Coroutine { + this.finally = VoidCallback(context, block) + return this@Coroutine + } + + fun onCancel( + context: CoroutineContext? = null, + block: suspend CoroutineScope.() -> Unit + ): Coroutine { + this.cancel = VoidCallback(context, block) + return this@Coroutine + } + + //取消当前任务 + fun cancel(cause: CancellationException? = null) { + job.cancel(cause) + cancel?.let { + MainScope().launch { + if (null == it.context) { + it.block.invoke(scope) + } else { + withContext(scope.coroutineContext.plus(it.context)) { + it.block.invoke(this) + } + } + } + } + } + + fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { + return job.invokeOnCompletion(handler) + } + + private fun executeInternal( + context: CoroutineContext, + block: suspend CoroutineScope.() -> T + ): Job { + return scope.plus(Dispatchers.Main).launch { + try { + start?.let { dispatchVoidCallback(this, it) } + val value = executeBlock(scope, context, timeMillis ?: 0L, block) + if (isActive) { + success?.let { dispatchCallback(this, value, it) } + } + } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + val consume: Boolean = errorReturn?.value?.let { value -> + if (isActive) { + success?.let { dispatchCallback(this, value, it) } + } + true + } ?: false + + if (!consume && isActive) { + error?.let { dispatchCallback(this, e, it) } + } + } finally { + if (isActive) { + finally?.let { dispatchVoidCallback(this, it) } + } + } + } + } + + private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) { + if (null == callback.context) { + callback.block.invoke(scope) + } else { + withContext(scope.coroutineContext.plus(callback.context)) { + callback.block.invoke(this) + } + } + } + + private suspend inline fun dispatchCallback( + scope: CoroutineScope, + value: R, + callback: Callback + ) { + if (!scope.isActive) return + if (null == callback.context) { + callback.block.invoke(scope, value) + } else { + withContext(scope.coroutineContext.plus(callback.context)) { + callback.block.invoke(this, value) + } + } + } + + private suspend inline fun executeBlock( + scope: CoroutineScope, + context: CoroutineContext, + timeMillis: Long, + noinline block: suspend CoroutineScope.() -> T + ): T { + return withContext(scope.coroutineContext.plus(context)) { + if (timeMillis > 0L) withTimeout(timeMillis) { + block() + } else block() + } + } + + private data class Result(val value: T?) + + private inner class VoidCallback( + val context: CoroutineContext?, + val block: suspend CoroutineScope.() -> Unit + ) + + private inner class Callback( + val context: CoroutineContext?, + val block: suspend CoroutineScope.(VALUE) -> Unit + ) +} diff --git a/app/src/main/java/com/novel/read/help/coroutine/CoroutineContainer.kt b/app/src/main/java/com/novel/read/help/coroutine/CoroutineContainer.kt new file mode 100644 index 0000000..f7756c1 --- /dev/null +++ b/app/src/main/java/com/novel/read/help/coroutine/CoroutineContainer.kt @@ -0,0 +1,16 @@ +package com.novel.read.help.coroutine + + +internal interface CoroutineContainer { + + fun add(coroutine: Coroutine<*>): Boolean + + fun addAll(vararg coroutines: Coroutine<*>): Boolean + + fun remove(coroutine: Coroutine<*>): Boolean + + fun delete(coroutine: Coroutine<*>): Boolean + + fun clear() + +} diff --git a/app/src/main/java/com/novel/read/http/AccountManager.kt b/app/src/main/java/com/novel/read/http/AccountManager.kt deleted file mode 100644 index 369fc63..0000000 --- a/app/src/main/java/com/novel/read/http/AccountManager.kt +++ /dev/null @@ -1,303 +0,0 @@ -package com.novel.read.http - -import android.content.Context -import android.text.TextUtils -import android.util.Log - -import com.mango.mangolib.event.EventManager -import com.mango.mangolib.http.MyRequestType -import com.mango.mangolib.http.ServiceCallback -import com.novel.read.event.AddBookSignEvent -import com.novel.read.event.BookArticleEvent -import com.novel.read.event.DeleteBookSignEvent -import com.novel.read.event.ErrorChapterEvent -import com.novel.read.event.FinishChapterEvent -import com.novel.read.event.GetBookDetailEvent -import com.novel.read.event.GetBookSignEvent -import com.novel.read.event.GetCategoryTypeEvent -import com.novel.read.event.GetRecommendBookEvent -import com.novel.read.event.HotSearchEvent -import com.novel.read.event.LoginEvent -import com.novel.read.event.SearchListEvent -import com.novel.read.event.VersionEvent -import com.novel.read.http.service.AccountService -import com.novel.read.model.db.ChapterInfoBean -import com.novel.read.model.db.dbManage.BookRepository -import com.novel.read.model.protocol.BookDetailResp -import com.novel.read.model.protocol.RankByUpdateResp -import com.novel.read.model.protocol.RecommendListResp -import com.novel.read.utlis.PhoneUtils -import com.novel.read.widget.page.TxtChapter - -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription - -import java.util.ArrayDeque -import java.util.ArrayList -import java.util.HashMap - -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import okhttp3.MediaType -import okhttp3.RequestBody -import retrofit2.Callback - -/** - * Created by zlj on 2017/2/14. - */ - -class AccountManager private constructor() { - - private val accountService: AccountService = - ServiceGenerator.createService(AccountService::class.java, MyRequestType.URL_TEXT) - - private var mChapterSub: Subscription? = null - - fun getRecommendBook(bookId: String, limit: String) { - val map = HashMap() - map["book_id"] = bookId - val call = accountService.getRecommendBook(getUrlString(Urls.getRecommend, map)) - call.enqueue(ServiceCallback(GetRecommendBookEvent::class.java)) - } - - fun getBookDetail(bookId: String) { - val map = HashMap() - map["book_id"] = bookId - val call = accountService.getBookDetail(getUrlString(Urls.getBookDetail, map)) - call.enqueue(ServiceCallback(GetBookDetailEvent::class.java)) - } - - fun getBookDetails(bookId: String): Single { //rxjava 获取多书籍详情 合并请求调用 - val map = HashMap() - map["book_id"] = bookId - return accountService.getBookDetails(getUrlString(Urls.getBookDetail, map)) - } - - fun getCategoryType() { - val map = HashMap() - val call = accountService.getCategoryType(getUrlString(Urls.getCategoryType, map)) - call.enqueue(ServiceCallback(GetCategoryTypeEvent::class.java)) - } - - fun getHotSearch() { - val map = HashMap() - val call = accountService.getHotSearch(getUrlString(Urls.getHotSearch, map)) - call.enqueue(ServiceCallback(HotSearchEvent::class.java)) - } - - fun getBookArticle(bookId: String, hasContent: String="2", page: String="1", limit: String="100000") { - val map = HashMap() - map["book_id"] = bookId - map["has_content"] = hasContent - map["page"] = page - map["limit"] = limit - val call = accountService.getBookArticle(getUrlString(Urls.getBookArticle, map)) - call.enqueue(ServiceCallback(BookArticleEvent::class.java)) - } - - - fun getSearchBookList(category_id: String, key: String, page: Int) { - val map = HashMap() - if (!TextUtils.isEmpty(category_id)) { - map["category_id"] = category_id - } else { - map["category_id"] = "0" - } - if (!TextUtils.isEmpty(key)) { - map["key"] = key - } - map["page"] = page.toString() - val call = accountService.getSearchList(getUrlString(Urls.getBookList, map)) - call.enqueue(ServiceCallback(SearchListEvent::class.java)) - } - - fun getRecommendList(listType: String, callback: Callback) { - val map = HashMap() - map["type"] = listType - val call = accountService.getRecommendList(getUrlString(Urls.getRecommendList, map)) - call.enqueue(callback) - - } - - fun getRankByUpdate(page: Int, limit: Int, callback: Callback) { - val map = HashMap() - if (!TextUtils.isEmpty(page.toString())) { - map["page"] = page.toString() - } - if (limit != 0) { - map["limit"] = limit.toString() - } - val call = accountService.getRankByUpdate(getUrlString(Urls.getRankByUpdate, map)) - call.enqueue(callback) - } - - fun getRankList( - type: String, - sex: String, - dateType: String, - page: String, - callback: Callback - ) { - val map = HashMap() - map["type"] = type - map["gender"] = sex - map["date_type"] = dateType - map["page"] = page - val call = accountService.getRankList(getUrlString(Urls.getRankList, map)) - call.enqueue(callback) - } - - fun checkVersion(versionCode: Int) { - val map = HashMap() - map["version"] = versionCode.toString() - // map.put("shell", Constant.shell); - val call = accountService.checkVersion(getUrlString(Urls.checkVersion, map)) - call.enqueue(ServiceCallback(VersionEvent::class.java)) - } - - fun addSign(bookId: String, articleId: String, content: String) { - val map = HashMap() - map["book_id"] = bookId - map["article_id"] = articleId - map["content"] = content - val call = accountService.addSign(Urls.addBookSign, mapToBody(map)) - call.enqueue(ServiceCallback(AddBookSignEvent::class.java)) - } - - fun deleteSign(signIds: String) { - val map = HashMap() - map["sign_ids"] = signIds - val call = accountService.deleteSign(Urls.deleteSign, mapToBody(map)) - call.enqueue(ServiceCallback(DeleteBookSignEvent::class.java)) - } - - fun getSignList(bookId: String) { - val map = HashMap() - map["book_id"] = bookId - val call = accountService.getSignList(getUrlString(Urls.getBookSign, map)) - call.enqueue(ServiceCallback(GetBookSignEvent::class.java)) - } - - fun login(mContext: Context) { - val map = HashMap() - Log.e("getUniquePsuedoID", "login: " + PhoneUtils.uniquePsuedoID) - map["code"] = PhoneUtils.uniquePsuedoID - val call = accountService.login(Urls.login, mapToBody(map)) - call.enqueue(ServiceCallback(LoginEvent::class.java)) - } - - fun getBookArticleDetail(bookId: String?, bookChapters: List) { - val size = bookChapters.size - //取消上次的任务,防止多次加载 - if (mChapterSub != null) { - mChapterSub!!.cancel() - } - val chapterInfos = ArrayList>(bookChapters.size) - val titles = ArrayDeque(bookChapters.size) - - // 将要下载章节,转换成网络请求。 - for (i in 0 until size) { - val bookChapter = bookChapters[i] - // 网络中获取数据 - val chapterInfoSingle = getChapterInfo(bookChapter.chapterId) - chapterInfos.add(chapterInfoSingle) - titles.add(bookChapter.title) - } - - Single.concat(chapterInfos) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Subscriber { - internal var title = titles.poll() - - override fun onSubscribe(s: Subscription) { - s.request(Integer.MAX_VALUE.toLong()) - mChapterSub = s - } - - override fun onNext(chapterInfoBean: ChapterInfoBean) { - //存储数据 - BookRepository.getInstance().saveChapterInfo( - bookId, title, chapterInfoBean.body - ) - EventManager.instance.postEvent(FinishChapterEvent()) - //将获取到的数据进行存储 - title = titles.poll() - } - - override fun onError(t: Throwable) { - //只有第一个加载失败才会调用errorChapter - if (bookChapters[0].title == title) { - EventManager.instance.postEvent(ErrorChapterEvent()) - } - } - - override fun onComplete() {} - } - ) - } - - /** - * 注意这里用的是同步请求 - */ - fun getChapterInfo(id: String): Single { - val map = HashMap() - map["article_id"] = id - return accountService.getBookArticleDetail(getUrlString(Urls.getDetail, map)) - .map { bean -> bean.article?.get(0) } - } - - /** - * get方法拼接字符串 - */ - private fun getUrlString(path: String, query: HashMap?): String { - var mypath = path - if (query != null && query.size > 0) { - val pathWithQuery = StringBuilder(path) - if (!path.contains("?")) { - pathWithQuery.append("?") - } else { - pathWithQuery.append("&") - } - - for (stringStringEntry in query.entries) { - val key = (stringStringEntry as Map.Entry<*, *>).key as String - val `val` = (stringStringEntry as Map.Entry<*, *>).value as String - pathWithQuery.append(key) - pathWithQuery.append("=") - pathWithQuery.append(`val`) - pathWithQuery.append("&") - } - - pathWithQuery.deleteCharAt(pathWithQuery.length - 1) - mypath = pathWithQuery.toString() - } - - return mypath - } - - /** - * 减少请求info类的数量,直接用map替代实体类,返回body - */ - private fun mapToBody(map: HashMap): RequestBody { - return RequestBody.create( - MediaType.parse("application/json; charset=utf-8"), - ServiceGenerator.formatResponse(map) - ) - } - - companion object { - - private var instance: AccountManager? = null - - @Synchronized - fun getInstance(): AccountManager { - if (instance == null) { - instance = AccountManager() - } - return instance as AccountManager - } - } - -} diff --git a/app/src/main/java/com/novel/read/http/CommonHeadersInterceptor.kt b/app/src/main/java/com/novel/read/http/CommonHeadersInterceptor.kt deleted file mode 100644 index a29d6d2..0000000 --- a/app/src/main/java/com/novel/read/http/CommonHeadersInterceptor.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.novel.read.http - -import com.common_lib.base.utils.SecurityUtils -import com.novel.read.constants.Constant -import com.novel.read.utlis.SpUtil - -import java.io.IOException - -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response - -/** - * Created by zlj on 2019/3/1. - */ -class CommonHeadersInterceptor : Interceptor { - - @Throws(IOException::class) - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val builder = request.newBuilder() - - val authKey = "Android" - val timeStamp = (System.currentTimeMillis() / 1000).toString() - val uid = SpUtil.getStringValue(Constant.Uid, "1") - builder.addHeader("Content-Type", "application/json") - builder.addHeader("UID", uid) - builder.addHeader("AUTHKEY", authKey) - builder.addHeader("TIMESTAMP", timeStamp) - - builder.addHeader("SIGN", SecurityUtils.getInstance().MD5Decode(authKey + timeStamp).toUpperCase()) - - return chain.proceed(builder.build()) - } -} diff --git a/app/src/main/java/com/novel/read/http/ServiceGenerator.kt b/app/src/main/java/com/novel/read/http/ServiceGenerator.kt deleted file mode 100644 index b17c361..0000000 --- a/app/src/main/java/com/novel/read/http/ServiceGenerator.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.novel.read.http - -import com.google.gson.GsonBuilder -import com.mango.mangolib.http.GsonUTCdateAdapter -import com.mango.mangolib.http.MyRequestType -import com.mango.mangolib.http.ResponseConverterFactory - -import java.util.Date -import java.util.concurrent.TimeUnit - -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory - - -object ServiceGenerator { - private const val API_BASE_URL_TEXT = "http://novel.duoduvip.com/" - - private val gson = GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(Date::class.java, GsonUTCdateAdapter()).create() - - - private val builderTEXT = Retrofit.Builder() - .baseUrl(API_BASE_URL_TEXT) - .client(okHttp) - .addConverterFactory(ResponseConverterFactory.create()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(GsonConverterFactory.create(gson)) - - private val okHttp: OkHttpClient - get() = OkHttpClient() - .newBuilder() - .addInterceptor(CommonHeadersInterceptor()) - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .build() - - fun createService(serviceClass: Class, type: MyRequestType): S { - val retrofit = builderTEXT.build() - return retrofit.create(serviceClass) - } - - fun formatResponse(obj: Any): String { - return gson.toJson(obj) - } -} diff --git a/app/src/main/java/com/novel/read/http/Urls.kt b/app/src/main/java/com/novel/read/http/Urls.kt deleted file mode 100644 index eb0731d..0000000 --- a/app/src/main/java/com/novel/read/http/Urls.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.novel.read.http - -/** - * create by zlj on 2019/10/14 - * describe: - */ - -object Urls { - - internal const val getRecommend = "api/book/getRecommendByBook/" //获取推荐书籍 - internal const val getBookDetail = "api/book/getBookDetail/" //获取书籍详情 - internal const val getCategoryType = "api/category/getCategoryList/" //获取小说分类 - internal const val getHotSearch = "api/search/getHotSearch/" //获取热搜墙 - internal const val getBookArticle = "api/book/getBookArticle/" //获取书籍章节 - - internal const val getBookList = "api/book/getBookList/" //获取和搜索书籍列表 - internal const val getRecommendList = "api/rank/getRecommendList/" //获取推荐列表 - internal const val getRankByUpdate = "api/rank/getRankByUpdate/" //最新更新 - internal const val getRankList = "api/rank/getRankList/" //获取排行榜 - internal const val checkVersion = "api/version/checkVersion/" //检测新版本 - internal const val addBookSign = "api/sign/addBookSign/" //添加书签 - internal const val getBookSign = "api/sign/getBookSignList/" //获取书签 - internal const val deleteSign = "api/sign/deleteSign/" //获取书签 - - internal const val login = "api/login/checkLogin"//登录 - - internal const val getDetail = "api/book/getArticleDetail"//获取章节详情 - -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/http/service/AccountService.kt b/app/src/main/java/com/novel/read/http/service/AccountService.kt deleted file mode 100644 index 0aa9754..0000000 --- a/app/src/main/java/com/novel/read/http/service/AccountService.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.novel.read.http.service - -import com.mango.mangolib.http.ErrorResponse -import com.novel.read.model.protocol.BookArticleResp -import com.novel.read.model.protocol.BookDetailResp -import com.novel.read.model.protocol.CategoryTypeResp -import com.novel.read.model.protocol.ChapterInfoPackage -import com.novel.read.model.protocol.HotSearchResp -import com.novel.read.model.protocol.MarkResp -import com.novel.read.model.protocol.RankByUpdateResp -import com.novel.read.model.protocol.RecommendBookResp -import com.novel.read.model.protocol.RecommendListResp -import com.novel.read.model.protocol.SearchResp -import com.novel.read.model.protocol.UidResp -import com.novel.read.model.protocol.VersionResp - -import io.reactivex.Single -import okhttp3.RequestBody -import retrofit2.Call -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Url - -/** - * Created by Administrator on 2017/2/14. - */ - -interface AccountService { - - //获取推荐书籍 - @GET - fun getRecommendBook(@Url url: String): Call - - //获取书籍详情 - @GET - fun getBookDetail(@Url url: String): Call - - //获取书籍详情 - @GET - fun getBookDetails(@Url url: String): Single - - //小说类型 - @GET - fun getCategoryType(@Url url: String): Call - - //热搜 - @GET - fun getHotSearch(@Url url: String): Call - - //书籍章节 - @GET - fun getBookArticle(@Url url: String): Call - - //搜索 - @GET - fun getSearchList(@Url url: String): Call - - //获取推荐 - @GET - fun getRecommendList(@Url url: String): Call - - //获取最新排行 - @GET - fun getRankByUpdate(@Url url: String): Call - - //获取排行 - @GET - fun getRankList(@Url url: String): Call - - //获取新版本 - @GET - fun checkVersion(@Url url: String): Call - - //添加标签 - @POST - fun addSign(@Url url: String, @Body body: RequestBody): Call - - //删除标签 - @POST - fun deleteSign(@Url url: String, @Body body: RequestBody): Call - - //获取标签 - @GET - fun getSignList(@Url url: String): Call - - //登录 - @POST - fun login(@Url url: String, @Body body: RequestBody): Call - - //获取书籍章节详情 - @GET - fun getBookArticleDetail(@Url url: String): Single -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/inter/OnLoadMoreListener.kt b/app/src/main/java/com/novel/read/inter/OnLoadMoreListener.kt deleted file mode 100644 index aa211fa..0000000 --- a/app/src/main/java/com/novel/read/inter/OnLoadMoreListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.novel.read.inter - -/** - * create by zlj on 2019/10/14 - * describe: - */ -interface OnLoadMoreListener { - fun onLoadMore() -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/ATH.kt b/app/src/main/java/com/novel/read/lib/ATH.kt new file mode 100644 index 0000000..36b0536 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/ATH.kt @@ -0,0 +1,254 @@ +package com.novel.read.lib + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.ActivityManager +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.view.View +import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR +import android.view.Window +import android.widget.EdgeEffect +import android.widget.ScrollView +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager.widget.ViewPager +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.novel.read.App +import com.novel.read.R +import com.novel.read.help.AppConfig +import com.novel.read.help.TintHelper +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.activity_main.view.* +import org.jetbrains.anko.backgroundColor + + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ATH { + + @SuppressLint("CommitPrefEdits") + fun didThemeValuesChange(context: Context, since: Long): Boolean { + return ThemeStore.isConfigured(context) && ThemeStore.prefs(context).getLong( + ThemeStorePrefKeys.VALUES_CHANGED, + -1 + ) > since + } + + fun setStatusBarColorAuto(activity: Activity, fullScreen: Boolean) { + val isTransparentStatusBar = AppConfig.isTransparentStatusBar + setStatusBarColor( + activity, + ThemeStore.statusBarColor(activity, isTransparentStatusBar), + isTransparentStatusBar, fullScreen + ) + } + + fun setStatusBarColor( + activity: Activity, + color: Int, + isTransparentStatusBar: Boolean, + fullScreen: Boolean + ) { + if (fullScreen) { + if (isTransparentStatusBar) { + activity.window.statusBarColor = Color.TRANSPARENT + } else { + activity.window.statusBarColor = activity.getCompatColor(R.color.status_bar_bag) + } + } else { + activity.window.statusBarColor = color + } + setLightStatusBarAuto(activity.window, color) + } + + fun setLightStatusBarAuto(window: Window, bgColor: Int) { + setLightStatusBar(window, ColorUtils.isColorLight(bgColor)) + } + + fun setLightStatusBar(window: Window, enabled: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val decorView = window.decorView + val systemUiVisibility = decorView.systemUiVisibility + if (enabled) { + decorView.systemUiVisibility = + systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else { + decorView.systemUiVisibility = + systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } + } + } + + fun setLightNavigationBar(activity: Activity, enabled: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val decorView = activity.window.decorView + var systemUiVisibility = decorView.systemUiVisibility + systemUiVisibility = if (enabled) { + systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + systemUiVisibility and SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } + decorView.systemUiVisibility = systemUiVisibility + } + } + + fun setNavigationBarColorAuto( + activity: Activity, + color: Int = ThemeStore.navigationBarColor(activity) + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.window.navigationBarColor = color + setLightNavigationBar(activity, ColorUtils.isColorLight(color)) + } + } + + fun setTaskDescriptionColorAuto(activity: Activity) { + setTaskDescriptionColor(activity, ThemeStore.primaryColor(activity)) + } + + fun setTaskDescriptionColor(activity: Activity, @ColorInt color: Int) { + val color1: Int + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + color1 = ColorUtils.stripAlpha(color) + @Suppress("DEPRECATION") + activity.setTaskDescription( + ActivityManager.TaskDescription( + activity.title as String, + null, + color1 + ) + ) + } + } + + fun setTint( + view: View, + @ColorInt color: Int, + isDark: Boolean = AppConfig.isNightTheme(view.context) + ) { + TintHelper.setTintAuto(view, color, false, isDark) + } + + fun setBackgroundTint( + view: View, @ColorInt color: Int, + isDark: Boolean = AppConfig.isNightTheme + ) { + TintHelper.setTintAuto(view, color, true, isDark) + } + + fun setAlertDialogTint(dialog: AlertDialog): AlertDialog { + dialog.window?.setBackgroundDrawable(getDialogBackground()) + val colorStateList = Selector.colorBuild() + .setDefaultColor(ThemeStore.accentColor(dialog.context)) + .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(dialog.context))) + .create() + if (dialog.getButton(AlertDialog.BUTTON_NEGATIVE) != null) { + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList) + } + if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList) + } + if (dialog.getButton(AlertDialog.BUTTON_NEUTRAL) != null) { + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setTextColor(colorStateList) + } + return dialog + } + + fun setEdgeEffectColor(view: RecyclerView?, @ColorInt color: Int) { + view?.edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() { + override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { + val edgeEffect = super.createEdgeEffect(view, direction) + edgeEffect.color = color + return edgeEffect + } + } + } + + fun setEdgeEffectColor(viewPager: ViewPager?, @ColorInt color: Int) { + try { + val clazz = ViewPager::class.java + for (name in arrayOf("mLeftEdge", "mRightEdge")) { + val field = clazz.getDeclaredField(name) + field.isAccessible = true + val edge = field.get(viewPager) + (edge as EdgeEffect).color = color + } + } catch (ignored: Exception) { + } + } + + fun setEdgeEffectColor(scrollView: ScrollView?, @ColorInt color: Int) { + try { + val clazz = ScrollView::class.java + for (name in arrayOf("mEdgeGlowTop", "mEdgeGlowBottom")) { + val field = clazz.getDeclaredField(name) + field.isAccessible = true + val edge = field.get(scrollView) + (edge as EdgeEffect).color = color + } + } catch (ignored: Exception) { + } + } + + //**************************************************************Directly*************************************************************// + + fun applyBottomNavigationColor(bottomBar: BottomNavigationView) { + bottomBar.apply { + val bgColor = context.bottomBackground + setBackgroundColor(bgColor) + val textIsDark = ColorUtils.isColorLight(bgColor) + val textColor = context.getSecondaryTextColor(textIsDark) + val colorStateList = Selector.colorBuild() + .setDefaultColor(textColor) + .setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create() + itemIconTintList = colorStateList + itemTextColor = colorStateList + } + } + + fun applyAccentTint(view: View?) { + view?.apply { + setTint(this, context.accentColor) + } + } + + fun applyBackgroundTint(view: View?) { + view?.apply { + if (background == null) { + backgroundColor = context.backgroundColor + } else { + setBackgroundTint(this, context.backgroundColor) + } + } + } + + fun applyEdgeEffectColor(view: View?) { + when (view) { + is RecyclerView -> view.edgeEffectFactory = DEFAULT_EFFECT_FACTORY + is ViewPager -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) + is ScrollView -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) + } + } + + fun getDialogBackground(): GradientDrawable { + val background = GradientDrawable() + background.cornerRadius = 3F.dp + background.setColor(App.INSTANCE.backgroundColor) + return background + } + + private val DEFAULT_EFFECT_FACTORY = object : RecyclerView.EdgeEffectFactory() { + override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { + val edgeEffect = super.createEdgeEffect(view, direction) + edgeEffect.color = ThemeStore.primaryColor(view.context) + return edgeEffect + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/ATHUtils.kt b/app/src/main/java/com/novel/read/lib/ATHUtils.kt new file mode 100644 index 0000000..35171bb --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/ATHUtils.kt @@ -0,0 +1,34 @@ +package com.novel.read.lib + +import android.content.Context +import androidx.annotation.AttrRes + +/** + * @author Aidan Follestad (afollestad) + */ +object ATHUtils { + + @JvmOverloads + fun resolveColor(context: Context, @AttrRes attr: Int, fallback: Int = 0): Int { + val a = context.theme.obtainStyledAttributes(intArrayOf(attr)) + return try { + a.getColor(0, fallback) + } catch (e: Exception) { + fallback + } finally { + a.recycle() + } + } + + @JvmOverloads + fun resolveFloat(context: Context, @AttrRes attr: Int, fallback: Float = 0.0f): Float { + val a = context.theme.obtainStyledAttributes(intArrayOf(attr)) + return try { + a.getFloat(0, fallback) + } catch (e: Exception) { + fallback + } finally { + a.recycle() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/Selector.kt b/app/src/main/java/com/novel/read/lib/Selector.kt new file mode 100644 index 0000000..630f7d2 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/Selector.kt @@ -0,0 +1,445 @@ +package com.novel.read.lib + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.StateListDrawable +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import androidx.annotation.DrawableRes +import androidx.annotation.IntDef +import androidx.core.content.ContextCompat + +@Suppress("unused") +object Selector { + fun shapeBuild(): ShapeSelector { + return ShapeSelector() + } + + fun colorBuild(): ColorSelector { + return ColorSelector() + } + + fun drawableBuild(): DrawableSelector { + return DrawableSelector() + } + + /** + * 形状ShapeSelector + * + * @author hjy + * created at 2017/12/11 22:26 + */ + class ShapeSelector { + + private var mShape: Int = 0 //the shape of background + private var mDefaultBgColor: Int = 0 //default background color + private var mDisabledBgColor: Int = 0 //state_enabled = false + private var mPressedBgColor: Int = 0 //state_pressed = true + private var mSelectedBgColor: Int = 0 //state_selected = true + private var mFocusedBgColor: Int = 0 //state_focused = true + private var mCheckedBgColor: Int = 0 //state_checked = true + private var mStrokeWidth: Int = 0 //stroke width in pixel + private var mDefaultStrokeColor: Int = 0 //default stroke color + private var mDisabledStrokeColor: Int = 0 //state_enabled = false + private var mPressedStrokeColor: Int = 0 //state_pressed = true + private var mSelectedStrokeColor: Int = 0 //state_selected = true + private var mFocusedStrokeColor: Int = 0 //state_focused = true + private var mCheckedStrokeColor: Int = 0 //state_checked = true + private var mCornerRadius: Int = 0 //corner radius + + private var hasSetDisabledBgColor = false + private var hasSetPressedBgColor = false + private var hasSetSelectedBgColor = false + private val hasSetFocusedBgColor = false + private var hasSetCheckedBgColor = false + + private var hasSetDisabledStrokeColor = false + private var hasSetPressedStrokeColor = false + private var hasSetSelectedStrokeColor = false + private var hasSetFocusedStrokeColor = false + private var hasSetCheckedStrokeColor = false + + @IntDef(GradientDrawable.RECTANGLE, GradientDrawable.OVAL, GradientDrawable.LINE, GradientDrawable.RING) + private annotation class Shape + + init { + //initialize default values + mShape = GradientDrawable.RECTANGLE + mDefaultBgColor = Color.TRANSPARENT + mDisabledBgColor = Color.TRANSPARENT + mPressedBgColor = Color.TRANSPARENT + mSelectedBgColor = Color.TRANSPARENT + mFocusedBgColor = Color.TRANSPARENT + mStrokeWidth = 0 + mDefaultStrokeColor = Color.TRANSPARENT + mDisabledStrokeColor = Color.TRANSPARENT + mPressedStrokeColor = Color.TRANSPARENT + mSelectedStrokeColor = Color.TRANSPARENT + mFocusedStrokeColor = Color.TRANSPARENT + mCornerRadius = 0 + } + + fun setShape(@Shape shape: Int): ShapeSelector { + mShape = shape + return this + } + + fun setDefaultBgColor(@ColorInt color: Int): ShapeSelector { + mDefaultBgColor = color + if (!hasSetDisabledBgColor) + mDisabledBgColor = color + if (!hasSetPressedBgColor) + mPressedBgColor = color + if (!hasSetSelectedBgColor) + mSelectedBgColor = color + if (!hasSetFocusedBgColor) + mFocusedBgColor = color + return this + } + + fun setDisabledBgColor(@ColorInt color: Int): ShapeSelector { + mDisabledBgColor = color + hasSetDisabledBgColor = true + return this + } + + fun setPressedBgColor(@ColorInt color: Int): ShapeSelector { + mPressedBgColor = color + hasSetPressedBgColor = true + return this + } + + fun setSelectedBgColor(@ColorInt color: Int): ShapeSelector { + mSelectedBgColor = color + hasSetSelectedBgColor = true + return this + } + + fun setFocusedBgColor(@ColorInt color: Int): ShapeSelector { + mFocusedBgColor = color + hasSetPressedBgColor = true + return this + } + + fun setCheckedBgColor(@ColorInt color: Int): ShapeSelector { + mCheckedBgColor = color + hasSetCheckedBgColor = true + return this + } + + fun setStrokeWidth(@Dimension width: Int): ShapeSelector { + mStrokeWidth = width + return this + } + + fun setDefaultStrokeColor(@ColorInt color: Int): ShapeSelector { + mDefaultStrokeColor = color + if (!hasSetDisabledStrokeColor) + mDisabledStrokeColor = color + if (!hasSetPressedStrokeColor) + mPressedStrokeColor = color + if (!hasSetSelectedStrokeColor) + mSelectedStrokeColor = color + if (!hasSetFocusedStrokeColor) + mFocusedStrokeColor = color + return this + } + + fun setDisabledStrokeColor(@ColorInt color: Int): ShapeSelector { + mDisabledStrokeColor = color + hasSetDisabledStrokeColor = true + return this + } + + fun setPressedStrokeColor(@ColorInt color: Int): ShapeSelector { + mPressedStrokeColor = color + hasSetPressedStrokeColor = true + return this + } + + fun setSelectedStrokeColor(@ColorInt color: Int): ShapeSelector { + mSelectedStrokeColor = color + hasSetSelectedStrokeColor = true + return this + } + + fun setCheckedStrokeColor(@ColorInt color: Int): ShapeSelector { + mCheckedStrokeColor = color + hasSetCheckedStrokeColor = true + return this + } + + fun setFocusedStrokeColor(@ColorInt color: Int): ShapeSelector { + mFocusedStrokeColor = color + hasSetFocusedStrokeColor = true + return this + } + + fun setCornerRadius(@Dimension radius: Int): ShapeSelector { + mCornerRadius = radius + return this + } + + fun create(): StateListDrawable { + val selector = StateListDrawable() + + //enabled = false + if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) { + val disabledShape = getItemShape( + mShape, mCornerRadius, + mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor + ) + selector.addState(intArrayOf(-android.R.attr.state_enabled), disabledShape) + } + + //pressed = true + if (hasSetPressedBgColor || hasSetPressedStrokeColor) { + val pressedShape = getItemShape( + mShape, mCornerRadius, + mPressedBgColor, mStrokeWidth, mPressedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_pressed), pressedShape) + } + + //selected = true + if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) { + val selectedShape = getItemShape( + mShape, mCornerRadius, + mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_selected), selectedShape) + } + + //focused = true + if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) { + val focusedShape = getItemShape( + mShape, mCornerRadius, + mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_focused), focusedShape) + } + + //checked = true + if (hasSetCheckedBgColor || hasSetCheckedStrokeColor) { + val checkedShape = getItemShape( + mShape, mCornerRadius, + mCheckedBgColor, mStrokeWidth, mCheckedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_checked), checkedShape) + } + + //default + val defaultShape = getItemShape( + mShape, mCornerRadius, + mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor + ) + selector.addState(intArrayOf(), defaultShape) + + return selector + } + + private fun getItemShape( + shape: Int, cornerRadius: Int, + solidColor: Int, strokeWidth: Int, strokeColor: Int + ): GradientDrawable { + val drawable = GradientDrawable() + drawable.shape = shape + drawable.setStroke(strokeWidth, strokeColor) + drawable.cornerRadius = cornerRadius.toFloat() + drawable.setColor(solidColor) + return drawable + } + } + + /** + * 资源DrawableSelector + * + * @author hjy + * created at 2017/12/11 22:34 + */ + @Suppress("MemberVisibilityCanBePrivate") + class DrawableSelector { + + private var mDefaultDrawable: Drawable? = null + private var mDisabledDrawable: Drawable? = null + private var mPressedDrawable: Drawable? = null + private var mSelectedDrawable: Drawable? = null + private var mFocusedDrawable: Drawable? = null + + private var hasSetDisabledDrawable = false + private var hasSetPressedDrawable = false + private var hasSetSelectedDrawable = false + private var hasSetFocusedDrawable = false + + init { + mDefaultDrawable = ColorDrawable(Color.TRANSPARENT) + } + + fun setDefaultDrawable(drawable: Drawable?): DrawableSelector { + mDefaultDrawable = drawable + if (!hasSetDisabledDrawable) + mDisabledDrawable = drawable + if (!hasSetPressedDrawable) + mPressedDrawable = drawable + if (!hasSetSelectedDrawable) + mSelectedDrawable = drawable + if (!hasSetFocusedDrawable) + mFocusedDrawable = drawable + return this + } + + fun setDisabledDrawable(drawable: Drawable?): DrawableSelector { + mDisabledDrawable = drawable + hasSetDisabledDrawable = true + return this + } + + fun setPressedDrawable(drawable: Drawable?): DrawableSelector { + mPressedDrawable = drawable + hasSetPressedDrawable = true + return this + } + + fun setSelectedDrawable(drawable: Drawable?): DrawableSelector { + mSelectedDrawable = drawable + hasSetSelectedDrawable = true + return this + } + + fun setFocusedDrawable(drawable: Drawable?): DrawableSelector { + mFocusedDrawable = drawable + hasSetFocusedDrawable = true + return this + } + + fun create(): StateListDrawable { + val selector = StateListDrawable() + if (hasSetDisabledDrawable) + selector.addState(intArrayOf(-android.R.attr.state_enabled), mDisabledDrawable) + if (hasSetPressedDrawable) + selector.addState(intArrayOf(android.R.attr.state_pressed), mPressedDrawable) + if (hasSetSelectedDrawable) + selector.addState(intArrayOf(android.R.attr.state_selected), mSelectedDrawable) + if (hasSetFocusedDrawable) + selector.addState(intArrayOf(android.R.attr.state_focused), mFocusedDrawable) + selector.addState(intArrayOf(), mDefaultDrawable) + return selector + } + + fun setDefaultDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setDefaultDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setDisabledDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setDisabledDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setPressedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setPressedDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setSelectedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setSelectedDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setFocusedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setFocusedDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + } + + /** + * 颜色ColorSelector + * + * @author hjy + * created at 2017/12/11 22:26 + */ + class ColorSelector { + + private var mDefaultColor: Int = 0 + private var mDisabledColor: Int = 0 + private var mPressedColor: Int = 0 + private var mSelectedColor: Int = 0 + private var mFocusedColor: Int = 0 + private var mCheckedColor: Int = 0 + + private var hasSetDisabledColor = false + private var hasSetPressedColor = false + private var hasSetSelectedColor = false + private var hasSetFocusedColor = false + private var hasSetCheckedColor = false + + init { + mDefaultColor = Color.BLACK + mDisabledColor = Color.GRAY + mPressedColor = Color.BLACK + mSelectedColor = Color.BLACK + mFocusedColor = Color.BLACK + } + + fun setDefaultColor(@ColorInt color: Int): ColorSelector { + mDefaultColor = color + if (!hasSetDisabledColor) + mDisabledColor = color + if (!hasSetPressedColor) + mPressedColor = color + if (!hasSetSelectedColor) + mSelectedColor = color + if (!hasSetFocusedColor) + mFocusedColor = color + return this + } + + fun setDisabledColor(@ColorInt color: Int): ColorSelector { + mDisabledColor = color + hasSetDisabledColor = true + return this + } + + fun setPressedColor(@ColorInt color: Int): ColorSelector { + mPressedColor = color + hasSetPressedColor = true + return this + } + + fun setSelectedColor(@ColorInt color: Int): ColorSelector { + mSelectedColor = color + hasSetSelectedColor = true + return this + } + + fun setFocusedColor(@ColorInt color: Int): ColorSelector { + mFocusedColor = color + hasSetFocusedColor = true + return this + } + + fun setCheckedColor(@ColorInt color: Int): ColorSelector { + mCheckedColor = color + hasSetCheckedColor = true + return this + } + + fun create(): ColorStateList { + val colors = intArrayOf( + if (hasSetDisabledColor) mDisabledColor else mDefaultColor, + if (hasSetPressedColor) mPressedColor else mDefaultColor, + if (hasSetSelectedColor) mSelectedColor else mDefaultColor, + if (hasSetFocusedColor) mFocusedColor else mDefaultColor, + if (hasSetCheckedColor) mCheckedColor else mDefaultColor, + mDefaultColor + ) + val states = arrayOfNulls(6) + states[0] = intArrayOf(-android.R.attr.state_enabled) + states[1] = intArrayOf(android.R.attr.state_pressed) + states[2] = intArrayOf(android.R.attr.state_selected) + states[3] = intArrayOf(android.R.attr.state_focused) + states[4] = intArrayOf(android.R.attr.state_checked) + states[5] = intArrayOf() + return ColorStateList(states, colors) + } + } +} diff --git a/app/src/main/java/com/novel/read/lib/ThemeStore.kt b/app/src/main/java/com/novel/read/lib/ThemeStore.kt new file mode 100644 index 0000000..4f335df --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/ThemeStore.kt @@ -0,0 +1,359 @@ +package com.novel.read.lib + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Color +import androidx.annotation.AttrRes +import androidx.annotation.CheckResult +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import com.novel.read.App +import com.novel.read.R +import com.novel.read.utils.ColorUtils +import io.legado.app.lib.theme.ThemeStoreInterface + +/** + * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused") +class ThemeStore @SuppressLint("CommitPrefEdits") +private constructor(private val mContext: Context) : ThemeStoreInterface { + private val mEditor: SharedPreferences.Editor + + init { + mEditor = prefs(mContext).edit() + } + + + override fun primaryColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR, color) + if (autoGeneratePrimaryDark(mContext)) + primaryColorDark(ColorUtils.darkenColor(color)) + return this + } + + override fun primaryColorRes(@ColorRes colorRes: Int): ThemeStore { + return primaryColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun primaryColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return primaryColor(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun primaryColorDark(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK, color) + return this + } + + override fun primaryColorDarkRes(@ColorRes colorRes: Int): ThemeStore { + return primaryColorDark(ContextCompat.getColor(mContext, colorRes)) + } + + override fun primaryColorDarkAttr(@AttrRes colorAttr: Int): ThemeStore { + return primaryColorDark(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun accentColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_ACCENT_COLOR, color) + return this + } + + override fun accentColorRes(@ColorRes colorRes: Int): ThemeStore { + return accentColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun accentColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return accentColor(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun statusBarColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, color) + return this + } + + override fun statusBarColorRes(@ColorRes colorRes: Int): ThemeStore { + return statusBarColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun statusBarColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return statusBarColor(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun navigationBarColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, color) + return this + } + + override fun navigationBarColorRes(@ColorRes colorRes: Int): ThemeStore { + return navigationBarColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun navigationBarColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return navigationBarColor(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorPrimary(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY, color) + return this + } + + override fun textColorPrimaryRes(@ColorRes colorRes: Int): ThemeStore { + return textColorPrimary(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorPrimaryAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorPrimary(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorPrimaryInverse(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY_INVERSE, color) + return this + } + + override fun textColorPrimaryInverseRes(@ColorRes colorRes: Int): ThemeStore { + return textColorPrimaryInverse(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorPrimaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorPrimaryInverse(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorSecondary(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY, color) + return this + } + + override fun textColorSecondaryRes(@ColorRes colorRes: Int): ThemeStore { + return textColorSecondary(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorSecondaryAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorSecondary(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorSecondaryInverse(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY_INVERSE, color) + return this + } + + override fun textColorSecondaryInverseRes(@ColorRes colorRes: Int): ThemeStore { + return textColorSecondaryInverse(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorSecondaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorSecondaryInverse(ATHUtils.resolveColor(mContext, colorAttr)) + } + + override fun backgroundColor(color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, color) + return this + } + + override fun bottomBackground(color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, color) + return this + } + + override fun coloredStatusBar(colored: Boolean): ThemeStore { + mEditor.putBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, colored) + return this + } + + override fun coloredNavigationBar(applyToNavBar: Boolean): ThemeStore { + mEditor.putBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARY_NAVBAR, applyToNavBar) + return this + } + + override fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore { + mEditor.putBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate) + return this + } + + // Commit method + + override fun apply() { + mEditor.putLong(ThemeStorePrefKeys.VALUES_CHANGED, System.currentTimeMillis()) + .putBoolean(ThemeStorePrefKeys.IS_CONFIGURED_KEY, true) + .apply() + } + + companion object { + + fun editTheme(context: Context): ThemeStore { + return ThemeStore(context) + } + + // Static getters + + @CheckResult + internal fun prefs(context: Context): SharedPreferences { + return context.getSharedPreferences( + ThemeStorePrefKeys.CONFIG_PREFS_KEY_DEFAULT, + Context.MODE_PRIVATE + ) + } + + fun markChanged(context: Context) { + ThemeStore(context).apply() + } + + @CheckResult + @ColorInt + fun primaryColor(context: Context = App.INSTANCE): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_PRIMARY_COLOR, + ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64")) + ) + } + + @CheckResult + @ColorInt + fun primaryColorDark(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK, + ATHUtils.resolveColor(context, R.attr.colorPrimaryDark, Color.parseColor("#37474F")) + ) + } + + @CheckResult + @ColorInt + fun accentColor(context: Context = App.INSTANCE): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_ACCENT_COLOR, + ATHUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238")) + ) + } + + @CheckResult + @ColorInt + fun statusBarColor(context: Context, transparent: Boolean): Int { + return if (!coloredStatusBar(context)) { + Color.BLACK + } else if (transparent) { + prefs(context).getInt( + ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, + primaryColor(context) + ) + } else { + prefs(context).getInt( + ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, + primaryColorDark(context) + ) + } + } + + @CheckResult + @ColorInt + fun navigationBarColor(context: Context): Int { + return if (!coloredNavigationBar(context)) { + Color.BLACK + } else prefs(context).getInt( + ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, + bottomBackground(context) + ) + } + + @CheckResult + @ColorInt + fun textColorPrimary(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY, + ATHUtils.resolveColor(context, android.R.attr.textColorPrimary) + ) + } + + @CheckResult + @ColorInt + fun textColorPrimaryInverse(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY_INVERSE, + ATHUtils.resolveColor(context, android.R.attr.textColorPrimaryInverse) + ) + } + + @CheckResult + @ColorInt + fun textColorSecondary(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY, + ATHUtils.resolveColor(context, android.R.attr.textColorSecondary) + ) + } + + @CheckResult + @ColorInt + fun textColorSecondaryInverse(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY_INVERSE, + ATHUtils.resolveColor(context, android.R.attr.textColorSecondaryInverse) + ) + } + + @CheckResult + @ColorInt + fun backgroundColor(context: Context = App.INSTANCE): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, + ATHUtils.resolveColor(context, android.R.attr.colorBackground) + ) + } + + @SuppressLint("PrivateResource") + @CheckResult + fun elevation(context: Context): Float { + return prefs(context).getFloat( + ThemeStorePrefKeys.KEY_ELEVATION, + ATHUtils.resolveFloat( + context, + android.R.attr.elevation, + context.resources.getDimension(R.dimen.design_appbar_elevation) + ) + ) + } + + @CheckResult + @ColorInt + fun bottomBackground(context: Context = App.INSTANCE): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, + ATHUtils.resolveColor(context, android.R.attr.colorBackground) + ) + } + + @CheckResult + fun coloredStatusBar(context: Context): Boolean { + return prefs(context).getBoolean( + ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, + true + ) + } + + @CheckResult + fun coloredNavigationBar(context: Context): Boolean { + return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARY_NAVBAR, false) + } + + @CheckResult + fun autoGeneratePrimaryDark(context: Context): Boolean { + return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, true) + } + + @CheckResult + fun isConfigured(context: Context): Boolean { + return prefs(context).getBoolean(ThemeStorePrefKeys.IS_CONFIGURED_KEY, false) + } + + @SuppressLint("CommitPrefEdits") + fun isConfigured(context: Context, version: Int): Boolean { + val prefs = prefs(context) + val lastVersion = prefs.getInt(ThemeStorePrefKeys.IS_CONFIGURED_VERSION_KEY, -1) + if (version > lastVersion) { + prefs.edit().putInt(ThemeStorePrefKeys.IS_CONFIGURED_VERSION_KEY, version).apply() + return false + } + return true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/ThemeStoreInterface.kt b/app/src/main/java/com/novel/read/lib/ThemeStoreInterface.kt new file mode 100644 index 0000000..ce2d6e8 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/ThemeStoreInterface.kt @@ -0,0 +1,97 @@ +package io.legado.app.lib.theme + + +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import com.novel.read.lib.ThemeStore + +/** + * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) + */ +internal interface ThemeStoreInterface { + + // Primary colors + + fun primaryColor(@ColorInt color: Int): ThemeStore + + fun primaryColorRes(@ColorRes colorRes: Int): ThemeStore + + fun primaryColorAttr(@AttrRes colorAttr: Int): ThemeStore + + fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore + + fun primaryColorDark(@ColorInt color: Int): ThemeStore + + fun primaryColorDarkRes(@ColorRes colorRes: Int): ThemeStore + + fun primaryColorDarkAttr(@AttrRes colorAttr: Int): ThemeStore + + // Accent colors + + fun accentColor(@ColorInt color: Int): ThemeStore + + fun accentColorRes(@ColorRes colorRes: Int): ThemeStore + + fun accentColorAttr(@AttrRes colorAttr: Int): ThemeStore + + // Status bar color + + fun statusBarColor(@ColorInt color: Int): ThemeStore + + fun statusBarColorRes(@ColorRes colorRes: Int): ThemeStore + + fun statusBarColorAttr(@AttrRes colorAttr: Int): ThemeStore + + // Navigation bar color + + fun navigationBarColor(@ColorInt color: Int): ThemeStore + + fun navigationBarColorRes(@ColorRes colorRes: Int): ThemeStore + + fun navigationBarColorAttr(@AttrRes colorAttr: Int): ThemeStore + + // Primary text color + + fun textColorPrimary(@ColorInt color: Int): ThemeStore + + fun textColorPrimaryRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorPrimaryAttr(@AttrRes colorAttr: Int): ThemeStore + + fun textColorPrimaryInverse(@ColorInt color: Int): ThemeStore + + fun textColorPrimaryInverseRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorPrimaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore + + // Secondary text color + + fun textColorSecondary(@ColorInt color: Int): ThemeStore + + fun textColorSecondaryRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorSecondaryAttr(@AttrRes colorAttr: Int): ThemeStore + + fun textColorSecondaryInverse(@ColorInt color: Int): ThemeStore + + fun textColorSecondaryInverseRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorSecondaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore + + // Background + + fun backgroundColor(@ColorInt color: Int): ThemeStore + + fun bottomBackground(@ColorInt color: Int): ThemeStore + + // Toggle configurations + + fun coloredStatusBar(colored: Boolean): ThemeStore + + fun coloredNavigationBar(applyToNavBar: Boolean): ThemeStore + + // Commit/apply + + fun apply() +} diff --git a/app/src/main/java/com/novel/read/lib/ThemeStorePrefKeys.kt b/app/src/main/java/com/novel/read/lib/ThemeStorePrefKeys.kt new file mode 100644 index 0000000..50fb152 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/ThemeStorePrefKeys.kt @@ -0,0 +1,32 @@ +package com.novel.read.lib + +/** + * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) + */ +object ThemeStorePrefKeys { + + const val CONFIG_PREFS_KEY_DEFAULT = "app_themes" + const val IS_CONFIGURED_KEY = "is_configured" + const val IS_CONFIGURED_VERSION_KEY = "is_configured_version" + const val VALUES_CHANGED = "values_changed" + + const val KEY_PRIMARY_COLOR = "primary_color" + const val KEY_PRIMARY_COLOR_DARK = "primary_color_dark" + const val KEY_ACCENT_COLOR = "accent_color" + const val KEY_STATUS_BAR_COLOR = "status_bar_color" + const val KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color" + + const val KEY_TEXT_COLOR_PRIMARY = "text_color_primary" + const val KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse" + const val KEY_TEXT_COLOR_SECONDARY = "text_color_secondary" + const val KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse" + + const val KEY_BACKGROUND_COLOR = "backgroundColor" + const val KEY_BOTTOM_BACKGROUND = "bottomBackground" + + const val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar" + const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar" + const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark" + + const val KEY_ELEVATION = "elevation" +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/dialogs/AlertBuilder.kt b/app/src/main/java/com/novel/read/lib/dialogs/AlertBuilder.kt new file mode 100644 index 0000000..8034d46 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/dialogs/AlertBuilder.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +package com.novel.read.lib.dialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.view.KeyEvent +import android.view.View +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.novel.read.R +import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER +import kotlin.DeprecationLevel.ERROR + +@SuppressLint("SupportAnnotationUsage") +interface AlertBuilder { + val ctx: Context + + var title: CharSequence + @Deprecated(NO_GETTER, level = ERROR) get + + var titleResource: Int + @Deprecated(NO_GETTER, level = ERROR) get + + var message: CharSequence + @Deprecated(NO_GETTER, level = ERROR) get + + var messageResource: Int + @Deprecated(NO_GETTER, level = ERROR) get + + var icon: Drawable + @Deprecated(NO_GETTER, level = ERROR) get + + @setparam:DrawableRes + var iconResource: Int + @Deprecated(NO_GETTER, level = ERROR) get + + var customTitle: View + @Deprecated(NO_GETTER, level = ERROR) get + + var customView: View + @Deprecated(NO_GETTER, level = ERROR) get + + var isCancelable: Boolean + @Deprecated(NO_GETTER, level = ERROR) get + + fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun positiveButton(@StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + + fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun negativeButton(@StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + + fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun neutralButton(@StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + + fun onCancelled(handler: (dialog: DialogInterface) -> Unit) + + fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) + + fun items(items: List, onItemSelected: (dialog: DialogInterface, index: Int) -> Unit) + fun items( + items: List, + onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit + ) + + fun multiChoiceItems( + items: Array, + checkedItems: BooleanArray, + onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit + ) + + fun singleChoiceItems( + items: Array, + checkedItem: Int = 0, + onClick: ((dialog: DialogInterface, which: Int) -> Unit)? = null + ) + + fun build(): D + fun show(): D +} + +fun AlertBuilder<*>.customTitle(view: () -> View) { + customTitle = view() +} + +fun AlertBuilder<*>.customView(view: () -> View) { + customView = view() +} + +inline fun AlertBuilder<*>.okButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = + positiveButton(android.R.string.ok, handler) + +inline fun AlertBuilder<*>.cancelButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = + negativeButton(android.R.string.cancel, handler) + +inline fun AlertBuilder<*>.yesButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = + positiveButton(R.string.yes, handler) + +inline fun AlertBuilder<*>.noButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = + negativeButton(R.string.no, handler) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/dialogs/AndroidAlertBuilder.kt b/app/src/main/java/com/novel/read/lib/dialogs/AndroidAlertBuilder.kt new file mode 100644 index 0000000..b4de991 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/dialogs/AndroidAlertBuilder.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.novel.read.lib.dialogs + +import android.content.Context +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.view.KeyEvent +import android.view.View +import androidx.appcompat.app.AlertDialog +import org.jetbrains.anko.internals.AnkoInternals +import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER +import kotlin.DeprecationLevel.ERROR + +val Android: AlertBuilderFactory = ::AndroidAlertBuilder + +internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder { + private val builder = AlertDialog.Builder(ctx) + + override var title: CharSequence + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setTitle(value) } + + override var titleResource: Int + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setTitle(value) } + + override var message: CharSequence + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setMessage(value) } + + override var messageResource: Int + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setMessage(value) } + + override var icon: Drawable + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setIcon(value) } + + override var iconResource: Int + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setIcon(value) } + + override var customTitle: View + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setCustomTitle(value) } + + override var customView: View + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setView(value) } + + override var isCancelable: Boolean + @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() + set(value) { builder.setCancelable(value) } + + override fun onCancelled(handler: (DialogInterface) -> Unit) { + builder.setOnCancelListener(handler) + } + + override fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) { + builder.setOnKeyListener(handler) + } + + override fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)?) { + builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun positiveButton(buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)?) { + builder.setPositiveButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)?) { + builder.setNegativeButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun negativeButton(buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)?) { + builder.setNegativeButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)?) { + builder.setNeutralButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun neutralButton(buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)?) { + builder.setNeutralButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun items(items: List, onItemSelected: (dialog: DialogInterface, index: Int) -> Unit) { + builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> + onItemSelected(dialog, which) + } + } + + override fun items( + items: List, + onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit + ) { + builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> + onItemSelected(dialog, items[which], which) + } + } + + override fun multiChoiceItems( + items: Array, + checkedItems: BooleanArray, + onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit + ) { + builder.setMultiChoiceItems(items, checkedItems) { dialog, which, isChecked -> + onClick(dialog, which, isChecked) + } + } + + override fun singleChoiceItems( + items: Array, + checkedItem: Int, + onClick: ((dialog: DialogInterface, which: Int) -> Unit)? + ) { + builder.setSingleChoiceItems(items, checkedItem) { dialog, which -> + onClick?.invoke(dialog, which) + } + } + + override fun build(): AlertDialog = builder.create() + + override fun show(): AlertDialog = builder.show() +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/lib/dialogs/AndroidDialogs.kt b/app/src/main/java/com/novel/read/lib/dialogs/AndroidDialogs.kt new file mode 100644 index 0000000..ab24d15 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/dialogs/AndroidDialogs.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused", "DEPRECATION") + +package com.novel.read.lib.dialogs + +import android.app.ProgressDialog +import android.content.Context +import android.content.DialogInterface +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import org.jetbrains.anko.AnkoContext + +inline fun Fragment.alert( + title: CharSequence? = null, + message: CharSequence? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = requireActivity().alert(title, message, init) + +fun Context.alert( + title: CharSequence? = null, + message: CharSequence? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertBuilder { + return AndroidAlertBuilder(this).apply { + if (title != null) { + this.title = title + } + if (message != null) { + this.message = message + } + if (init != null) init() + } +} + +inline fun Fragment.alert( + titleResource: Int? = null, + message: Int? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = requireActivity().alert(titleResource, message, init) + +fun Context.alert( + titleResource: Int? = null, + messageResource: Int? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertBuilder { + return AndroidAlertBuilder(this).apply { + if (titleResource != null) { + this.titleResource = titleResource + } + if (messageResource != null) { + this.messageResource = messageResource + } + if (init != null) init() + } +} + + +inline fun AnkoContext<*>.alert(noinline init: AlertBuilder.() -> Unit) = ctx.alert(init) +inline fun Fragment.alert(noinline init: AlertBuilder.() -> Unit) = requireContext().alert(init) + +fun Context.alert(init: AlertBuilder.() -> Unit): AlertBuilder = + AndroidAlertBuilder(this).apply { init() } + +inline fun Fragment.progressDialog( + title: Int? = null, + message: Int? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().progressDialog(title, message, init) + +fun Context.progressDialog( + title: Int? = null, + message: Int? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, false, init) + + +inline fun Fragment.indeterminateProgressDialog( + title: Int? = null, + message: Int? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().indeterminateProgressDialog(title, message, init) + +fun Context.indeterminateProgressDialog( + title: Int? = null, + message: Int? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, true, init) + +inline fun Fragment.progressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().progressDialog(title, message, init) + +fun Context.progressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title, message, false, init) + + +inline fun Fragment.indeterminateProgressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().indeterminateProgressDialog(title, message, init) + +fun Context.indeterminateProgressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title, message, true, init) + + +private fun Context.progressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + indeterminate: Boolean, + init: (ProgressDialog.() -> Unit)? = null +) = ProgressDialog(this).apply { + isIndeterminate = indeterminate + if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) + if (message != null) setMessage(message) + if (title != null) setTitle(title) + if (init != null) init() + show() +} diff --git a/app/src/main/java/com/novel/read/lib/dialogs/AndroidSelectors.kt b/app/src/main/java/com/novel/read/lib/dialogs/AndroidSelectors.kt new file mode 100644 index 0000000..bf3c903 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/dialogs/AndroidSelectors.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +package com.novel.read.lib.dialogs + +import android.content.Context +import android.content.DialogInterface +import androidx.fragment.app.Fragment +import com.novel.read.utils.ext.applyTint + +inline fun Fragment.selector( + title: CharSequence? = null, + items: List, + noinline onClick: (DialogInterface, Int) -> Unit +) = activity?.selector(title, items, onClick) + +fun Context.selector( + title: CharSequence? = null, + items: List, + onClick: (DialogInterface, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + if (title != null) { + this.title = title + } + items(items, onClick) + show().applyTint() + } +} + +fun Context.selector( + titleSource: Int? = null, + items: List, + onClick: (DialogInterface, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + if (titleSource != null) { + this.title = getString(titleSource) + } + items(items, onClick) + show().applyTint() + } +} diff --git a/app/src/main/java/com/novel/read/lib/dialogs/Dialogs.kt b/app/src/main/java/com/novel/read/lib/dialogs/Dialogs.kt new file mode 100644 index 0000000..5509d49 --- /dev/null +++ b/app/src/main/java/com/novel/read/lib/dialogs/Dialogs.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +package com.novel.read.lib.dialogs + +import android.content.Context +import android.content.DialogInterface +import androidx.fragment.app.Fragment + +typealias AlertBuilderFactory = (Context) -> AlertBuilder + +inline fun Fragment.alert( + noinline factory: AlertBuilderFactory, + title: String? = null, + message: String? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = activity?.alert(factory, title, message, init) + +fun Context.alert( + factory: AlertBuilderFactory, + title: String? = null, + message: String? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertBuilder { + return factory(this).apply { + if (title != null) { + this.title = title + } + if (message != null) { + this.message = message + } + if (init != null) init() + } +} + +inline fun Fragment.alert( + noinline factory: AlertBuilderFactory, + titleResource: Int? = null, + messageResource: Int? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = requireActivity().alert(factory, titleResource, messageResource, init) + +fun Context.alert( + factory: AlertBuilderFactory, + titleResource: Int? = null, + messageResource: Int? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertBuilder { + return factory(this).apply { + if (titleResource != null) { + this.titleResource = titleResource + } + if (messageResource != null) { + this.messageResource = messageResource + } + if (init != null) init() + } +} + +inline fun Fragment.alert( + noinline factory: AlertBuilderFactory, + noinline init: AlertBuilder.() -> Unit +) = requireActivity().alert(factory, init) + +fun Context.alert( + factory: AlertBuilderFactory, + init: AlertBuilder.() -> Unit +): AlertBuilder = factory(this).apply { init() } diff --git a/app/src/main/java/com/novel/read/model/db/BookChapterBean.java b/app/src/main/java/com/novel/read/model/db/BookChapterBean.java deleted file mode 100644 index a325c20..0000000 --- a/app/src/main/java/com/novel/read/model/db/BookChapterBean.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.novel.read.model.db; - -import org.litepal.crud.LitePalSupport; - -import java.io.Serializable; - -/** - * Created by zlj - * 书的章节链接(作为下载的进度数据) - * 同时作为网络章节和本地章节 (没有找到更好分离两者的办法) - */ -public class BookChapterBean extends LitePalSupport implements Serializable { - /** - * title : 第一章 他叫白小纯 - * link : http://read.qidian.com/chapter/rJgN8tJ_cVdRGoWu-UQg7Q2/6jr-buLIUJSaGfXRMrUjdw2 - * unreadble : false - */ - private String chapterId; - - private String link; - - private String title; - - //所属的下载任务 - private String taskName; - - private boolean unreadble; - - //所属的书籍 - private String bookId; - - //本地书籍参数 - private CollBookBean collBookBean; - - //本地下载参数 - private DownloadTaskBean downloadTaskBean; - - //在书籍文件中的起始位置 - private long start; - - //在书籍文件中的终止位置 - private long end; - - public BookChapterBean(String id, String link, String title, String taskName, - boolean unreadble, String bookId, long start, long end) { - this.chapterId = id; - this.link = link; - this.title = title; - this.taskName = taskName; - this.unreadble = unreadble; - this.bookId = bookId; - this.start = start; - this.end = end; - } - public BookChapterBean(String id,String title) { - this.chapterId = id; - this.title = title; - } - - public BookChapterBean() { - } - - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getLink() { - return link; - } - - public void setLink(String link) { - this.link = link; - } - - public boolean isUnreadble() { - return unreadble; - } - - public void setUnreadble(boolean unreadble) { - this.unreadble = unreadble; - } - - public String getTaskName() { - return taskName; - } - - public void setTaskName(String taskName) { - this.taskName = taskName; - } - - public boolean getUnreadble() { - return this.unreadble; - } - - public String getBookId() { - return bookId; - } - - public void setBookId(String bookId) { - this.bookId = bookId; - } - - public String getId() { - return chapterId; - } - - public void setId(String id) { - this.chapterId = id; - } - - public long getStart() { - return start; - } - - public void setStart(long start) { - this.start = start; - } - - public long getEnd() { - return end; - } - - public void setEnd(long end) { - this.end = end; - } - - public CollBookBean getCollBookBean() { - return collBookBean; - } - - public void setCollBookBean(CollBookBean collBookBean) { - this.collBookBean = collBookBean; - } - - public DownloadTaskBean getDownloadTaskBean() { - return downloadTaskBean; - } - - public void setDownloadTaskBean(DownloadTaskBean downloadTaskBean) { - this.downloadTaskBean = downloadTaskBean; - } - - @Override - public String toString() { - return "BookChapterBean{" + - "chapterId='" + chapterId + '\'' + - ", link='" + link + '\'' + - ", title='" + title + '\'' + - ", taskName='" + taskName + '\'' + - ", unreadble=" + unreadble + - ", bookId='" + bookId + '\'' + - ", start=" + start + - ", end=" + end + - '}'; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/model/db/BookRecordBean.kt b/app/src/main/java/com/novel/read/model/db/BookRecordBean.kt deleted file mode 100644 index 09ff07b..0000000 --- a/app/src/main/java/com/novel/read/model/db/BookRecordBean.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.novel.read.model.db - -import org.litepal.crud.LitePalSupport - -import java.io.Serializable - -/** - * Created by zlj - */ -class BookRecordBean : LitePalSupport(), Serializable { - //所属的书的id - var bookId: String = "" - //阅读到了第几章 - var chapter: Int = 0 - //当前的页码 - var pagePos: Int = 0 -} diff --git a/app/src/main/java/com/novel/read/model/db/BookSignTable.kt b/app/src/main/java/com/novel/read/model/db/BookSignTable.kt deleted file mode 100644 index 530cf2a..0000000 --- a/app/src/main/java/com/novel/read/model/db/BookSignTable.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.novel.read.model.db - -import org.litepal.crud.LitePalSupport - -import java.io.Serializable - -/** - * create by zlj on 2019/11/6 - * describe: 书签数据库 - */ -class BookSignTable(val bookId: String, val articleId: String, val content: String) : - LitePalSupport(), Serializable { - val saveTime: Long = System.currentTimeMillis() - var edit: Boolean = false -} diff --git a/app/src/main/java/com/novel/read/model/db/ChapterInfoBean.java b/app/src/main/java/com/novel/read/model/db/ChapterInfoBean.java deleted file mode 100644 index 76ffd48..0000000 --- a/app/src/main/java/com/novel/read/model/db/ChapterInfoBean.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.novel.read.model.db; - -public class ChapterInfoBean { - /** - * title : 第一章 他叫白小纯 - * content : 帽儿山,位于东林山脉中,山下有一个村子,民风淳朴,以耕田为生,与世隔绝。 - 清晨,村庄的大门前,整个村子里的乡亲,正为一个十五六岁少年送别,这少年瘦弱,但却白白净净,看起来很是乖巧,衣着尽管是寻常的青衫,可却洗的泛白,穿在这少年的身上,与他目中的纯净搭配在一起,透出一股子灵动。 - 他叫白小纯。 - “父老乡亲们,我要去修仙了,可我舍不得你们啊。”少年满脸不舍,原本就乖巧的样子,此刻看起来更为纯朴。 - 四周的乡亲,面面相觑,顿时摆出难舍之色。 - “小纯,你爹娘走的早,你是个……好孩子!!难道你不想长生了么,成为仙人就可以长生,能活的很久很久,走吧,雏鹰长大,总有飞出去的那一天。”人群内走出一个头发花白的老者,说道好孩子三个字时,他顿了一下。 - “在外面遇到任何事情,都要坚持下去,走出村子,就不要回来,因为你的路在前方!”老人神色慈祥,拍了拍少年的肩膀。 - “长生……”白小纯身体一震,目中慢慢坚定起来,在老者以及四周乡亲鼓励的目光下,他重重的点了点头,深深的看了一眼四周的乡亲,转身迈着大步,渐渐走出了村子。 - 眼看少年的身影远去,村中的众人,一个个都激动起来,目中的难舍刹那就被喜悦代替,那之前满脸慈祥的老者,此刻也在颤抖,眼中流下泪水。 - “苍天有眼,这白鼠狼,他终于……终于走了,是谁告诉他在附近看到仙人的,你为村子立下了大功!” - “这白鼠狼终于肯离开了,可怜我家的几只鸡,就因为这白鼠狼怕鸡打鸣,不知用了什么方法,唆使一群孩子吃鸡肉,把全村的鸡都给吃的干干净净……” - “今天过年了!”欢呼之声,立刻在这不大的村子里,沸腾而起,甚至有人拿出了锣鼓,高兴的敲打起来。 - 村子外,白小纯还没等走远,他就听到了身后村子内,传出了敲锣打鼓的声音,还夹着欢呼。 - 白小纯脚步一顿,神色有些古怪,干咳一声,伴随着耳边传来的锣鼓,白小纯顺着山路,走上了帽儿山。 - 这帽儿山虽不高,却灌木杂多,虽是清晨,可看起来也是黑压压一片,很是安静。 - “听二狗说,他前几天在这里被一头野猪追赶时,看到天上有仙人飞过……”白小纯走在山路上,心脏怦怦跳动时,忽然一旁的灌林中传来阵阵哗哗声,似野猪一样,这声音来的突然,让本就紧张的白小纯,顿时背后发凉。 - “谁,谁在那里!”白小纯右手快速从行囊中拿出四把斧头,六把柴刀,还觉得不放心,又从怀里取出了一小根黑色的香,死死的抓住。 - “别出来,千万别出来,我有斧头,有柴刀,手里的香还可以召唤天雷,能引仙人降临,你敢出来,就劈死你!”白小纯哆嗦的大喊,连滚带爬的夹着那些武器,赶紧顺着山路跑去,沿途叮当乱响,斧头柴刀掉了一地。 - 或许是真的被他给吓住了,很快的哗哗声就消失,没有什么野兽跑出来,白小纯面色苍白,擦了擦冷汗,有心放弃继续上山,可一想到手中这根香是他爹娘去世前留给他的,据说是祖上曾偶然的救下一个落魄的仙人,那仙人离去时留下这根香作为报答,曾言会收下白家血脉一人为弟子,只要点燃,仙人就会到来。 - 可至今为止,这根香他点过十多次,始终不见仙人到来,让白小纯开始怀疑仙人是不是真的会来,这一次之所以下定决心,一方面是香所剩不多,另一方面是他听村子里人说,头几天在这看到有仙人从天上飞过。 - 所以他这才到来,想着距离仙人近一些,或许仙人就察觉到了也说不定。 - 踌躇一番,白小纯咬牙继续,好在此山不高,不久他气喘吁吁的到了山顶,站在那里,他遥望山下的村庄,神色颇为感慨,又低头看着手中的只有指甲盖大小的黑香,此香似乎被燃烧了好多次,所剩不多。 - “三年了,爹娘保佑我,这次一定要成功!”白小纯深吸口气,小心的将香点燃,立刻四周狂风顿起,天空更是眨眼间乌云密布,一道道闪电划过,还有震耳欲聋的雷鸣在白小纯耳边直接炸开。 - 声音之大,气势之强,让白小纯身体哆嗦,有种随时会被雷劈死的感觉,下意识的就想要吐口唾沫将那根香灭掉,但却挣扎忍住。 - “三年了,我点这根香点了十二次,这是第十三次,这次一定要忍住,小纯不怕,应该不会被劈死……”白小纯想起了这三年的经历,不算这次,点了十二次,每次都是这样的雷鸣闪电,仙人也没有到来,吓的本就怕死的他每次都吐口唾沫将其熄灭,说来也怪,这根香看似不凡,可实际上一样是浇水就灭。 - 在白小纯这里心惊肉跳,艰难的于那雷声中等待时,距离这里不远处的天空上,有一道长虹正急速的呼啸而来。 - 长虹内是一个中年男子,这男子衣着华丽,仙风道骨,可偏偏风尘仆仆,甚至仔细去看,可以看到他神色内深深的疲惫。 - “我倒要看看,到底是个什么样的人,竟然点根香点了三年!” - 一想到自己这三年的经历,中年男子就气恼,三年前他察觉有人点燃自己还是凝气时送出的香药,想起了当年在凡俗中的一段人情。 - 这才飞出寻来,原本按照他的打算,很快就会回来,可没成想,刚寻着香气过去,还没等多远,那气息就瞬间消失,断了联系。若是一次也就罢了,这三年,气息出现了十多次。 - 使得他这里,多次在寻找时中断,就这样来来回回,折腾了三年…… - 此刻他遥遥的看到了帽儿山,看到了山顶上白小纯,气不打一处来,一瞬飞出,直接就站在了山顶,大手一挥,那根所剩不多的香,直接熄灭。 - 雷声刹那消失,白小纯愣了一下,抬头一看,看到了自己的身边多了一个中年男子。 - “仙人?”白小纯小心翼翼的开口,有些拿不准,背后偷偷捡起一把斧头。 - “本座李青候,你是白家后人?”中年修士目光如电,无视白小纯身后的斧子,打量了白小纯一番,觉得眼前此子眉清目秀,依稀与当年的故人相似,资质也不错,心底的恼意,也不由缓了一些。 - “晚辈正是白家后人,白小纯。”白小纯眨了眨眼,小声说道,虽然心中有些畏惧,但还是挺了挺腰板。 - “我问你,点一根香,为什么点了三年!”中年修士淡淡开口,问出了他这三年里,最想要知道的问题。 - 白小纯听到这个问题,脑筋飞速转动,然后脸上摆出惆怅,遥望山下的村庄。 - “晚辈是一个重情重义的人,舍不得那些乡亲们,每一次我点燃香,他们也都不舍得我离去,如今山下的他们,还在因为我的离去而悲伤呢。” - 中年修士一愣,这个缘由,是他之前没想到的,目中的恼色又少了一些,单单从话语上看,此子的本性还是不错的。 - 可当他的目光落在山下的村子时,他的神识随之扫过,听到了村子里的敲锣打鼓以及那一句句欢呼白鼠狼离去的话语,面色立刻难看起来,有些头疼,看着眼前这个外表乖巧纯朴,人畜无害的白小纯,已心底明朗对方实际上一肚子坏水。 - “说实话!”中年修士一瞪眼,声音如同雷声一样,白小纯吓得一个哆嗦。 - “这不怨我啊,你那什么破香啊,每次点燃都会打雷,好几次都差点劈死我,我躲过了十三次,已经很不容易了。”白小纯可怜兮兮的说道。 - 中年修士看着白小纯,半晌无语。 - “既然你这么害怕,为什么还要强行去点香十多次?”中年修士缓缓开口。 - “我怕死啊,修仙不是能长生么,我想长生啊。”白小纯委屈的说道。 - 中年修士再次无语,不过觉得此子总算执念可嘉,扔到门派里磨炼一番,或可在性子上改变一二。 - 于是略一思索,大袖一甩卷着白小纯化作一道长虹,直奔天边而去。 - “跟我走吧。” - “去哪?这也太高了吧……”白小纯看到自己在天上飞,下面是万丈深渊,立刻脸色苍白,斧头一扔,死死的抱住仙人的大腿。 - 中年修士看了眼自己的腿,无奈开口。 - “灵溪宗。” - 兄弟姐妹们,阔别2个月,你们想不想我啊,我非常想你们! - 这本书,我做了详细的大纲,每次回顾大纲里的情节,都很兴奋,有种燃烧的感觉,我非常满意,明天,正式更新,依旧是中午一章,晚上一章! - 很兴奋,我们已沉寂了数月,如今归来,要……再战起点! - 新书期,兄弟姐妹,别忘了收藏与推荐啊,收藏与推荐至关重要! - 求收藏!!求推荐!! - 让众人知晓,我们……归来了! - 我们的目标,依旧是……点击榜,推荐榜,第一! - */ - - private String title; - private String content; - private int words; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getBody() { - return content; - } - - public void setBody(String body) { - this.content = body; - } - - public int getWords() { - return words; - } - - public void setWords(int words) { - this.words = words; - } -} diff --git a/app/src/main/java/com/novel/read/model/db/CollBookBean.java b/app/src/main/java/com/novel/read/model/db/CollBookBean.java deleted file mode 100644 index f53a0fb..0000000 --- a/app/src/main/java/com/novel/read/model/db/CollBookBean.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.novel.read.model.db; - -import com.novel.read.base.MyApp; -import com.novel.read.utlis.StringUtils; - -import org.litepal.crud.LitePalSupport; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -/** - * 收藏的书籍 - */ -public class CollBookBean extends LitePalSupport implements Serializable { - - - /** - * _id : 53663ae356bdc93e49004474 - * title : 逍遥派 - * author : 白马出淤泥 - * shortIntro : 金庸武侠中有不少的神秘高手,书中或提起名字,或不曾提起,总之他们要么留下了绝世秘笈,要么就名震武林。 独孤九剑的创始者,独孤求败,他真的只创出九剑吗? 残本葵花... - * cover : /cover/149273897447137 - * hasCp : true - * latelyFollower : 60213 - * retentionRatio : 22.87 - * updated : 2017-05-07T18:24:34.720Z - *

- * chaptersCount : 1660 - * lastChapter : 第1659章 朱长老 - */ - private String bookId; // 本地书籍中,path 的 md5 值作为本地书籍的 id - - private String title; - private String author; - private String shortIntro; - private String cover; // 在本地书籍中,该字段作为本地文件的路径 - private boolean hasCp; - private int latelyFollower; - private double retentionRatio; - //最新更新日期 - private String updated; - //最新阅读日期 - private String lastRead; - private int chaptersCount; - private String lastChapter; - //是否更新或未阅读 - private boolean isUpdate = true; - //是否是本地文件 - private boolean isLocal = false; - private boolean isSelect = false; - private int include_image; - - public boolean isSelect() { - return isSelect; - } - - public void setSelect(boolean select) { - isSelect = select; - } - - private List bookChapterList = new ArrayList(); - - - public String getId() { - return bookId == null ? "" : bookId; - } - - public void setId(String id) { - this.bookId = id; - } - - public String getTitle() { - return title == null ? "" : StringUtils.INSTANCE.convertCC(title); - } - - public void setTitle(String title) { - this.title = title; - } - - public String getAuthor() { - return author == null ? "" : StringUtils.INSTANCE.convertCC(author); - } - - public void setAuthor(String author) { - this.author = author; - } - - public String getShortIntro() { - return shortIntro == null ? "" : StringUtils.INSTANCE.convertCC(shortIntro); - } - - public void setShortIntro(String shortIntro) { - this.shortIntro = shortIntro; - } - - public String getCover() { - return cover == null ? "" : cover; - } - - public void setCover(String cover) { - this.cover = cover; - } - - public boolean isHasCp() { - return hasCp; - } - - public void setHasCp(boolean hasCp) { - this.hasCp = hasCp; - } - - public int getLatelyFollower() { - return latelyFollower; - } - - public void setLatelyFollower(int latelyFollower) { - this.latelyFollower = latelyFollower; - } - - public double getRetentionRatio() { - return retentionRatio; - } - - public void setRetentionRatio(double retentionRatio) { - this.retentionRatio = retentionRatio; - } - - public String getUpdated() { - return updated == null ? "" : updated; - } - - public void setUpdated(String updated) { - this.updated = updated; - } - - public String getLastRead() { - return lastRead == null ? "" : lastRead; - } - - public void setLastRead(String lastRead) { - this.lastRead = lastRead; - } - - public int getChaptersCount() { - return chaptersCount; - } - - public void setChaptersCount(int chaptersCount) { - this.chaptersCount = chaptersCount; - } - - public String getLastChapter() { - return lastChapter == null ? "" : StringUtils.INSTANCE.convertCC(lastChapter); - } - - public void setLastChapter(String lastChapter) { - this.lastChapter = lastChapter; - } - - public boolean isUpdate() { - return isUpdate; - } - - public void setIsUpdate(boolean update) { - isUpdate = update; - } - - public boolean isLocal() { - return isLocal; - } - - public void setLocal(boolean local) { - isLocal = local; - } - - public int getInclude_image() { - return include_image; - } - - public void setInclude_image(int include_image) { - this.include_image = include_image; - } - - public List getBookChapters() { - if (bookChapterList == null) { - return new ArrayList<>(); - } - return bookChapterList; - } - - public void setBookChapters(List bookChapterList) { - this.bookChapterList = bookChapterList; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/model/db/DownloadTaskBean.java b/app/src/main/java/com/novel/read/model/db/DownloadTaskBean.java deleted file mode 100644 index dc64f21..0000000 --- a/app/src/main/java/com/novel/read/model/db/DownloadTaskBean.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.novel.read.model.db; - -import org.litepal.LitePal; -import org.litepal.crud.LitePalSupport; - -import java.util.ArrayList; -import java.util.List; - -/** - * Created by zlj - */ -public class DownloadTaskBean extends LitePalSupport { - public static final int STATUS_LOADING = 1; - public static final int STATUS_WAIT = 2; - public static final int STATUS_PAUSE = 3; - public static final int STATUS_ERROR = 4; - public static final int STATUS_FINISH = 5; - - //任务名称 -> 名称唯一不重复 - private String taskName; - //所属的bookId(外健) - private String bookId; - - private List bookChapterList = new ArrayList<>(); - //章节的下载进度,默认为初始状态 - private int currentChapter = 0; - //最后的章节 - private int lastChapter = 0; - //状态:正在下载、下载完成、暂停、等待、下载错误。 - - private volatile int status = STATUS_WAIT; - //总大小 -> (完成之后才会赋值) - private long size = 0; - private CollBookBean collBookBean; - - - public DownloadTaskBean(String taskName, String bookId, int currentChapter, int lastChapter, - int status, long size) { - this.taskName = taskName; - this.bookId = bookId; - this.currentChapter = currentChapter; - this.lastChapter = lastChapter; - this.status = status; - this.size = size; - } - - public DownloadTaskBean() { - } - - public String getBookId() { - return bookId; - } - - public void setBookId(String bookId) { - this.bookId = bookId; - } - - public String getTaskName() { - return taskName; - } - - public void setTaskName(String taskName) { - this.taskName = taskName; - if (bookChapterList!=null){ - for (BookChapterBean bean : bookChapterList){ - bean.setTaskName(getTaskName()); - } - } - } - - public List getBookChapterList() { - if (bookChapterList == null) { - List bookChapterListNew = LitePal - .where("taskName=?", taskName).find(BookChapterBean.class); - - synchronized (this) { - if (bookChapterList == null) { - bookChapterList = bookChapterListNew; - } - } - } - return bookChapterList; - } - - /** - * 这才是真正的列表使用类。 - * - */ - public void setBookChapters(List beans){ - bookChapterList = beans; - for (BookChapterBean bean : bookChapterList){ - bean.setTaskName(getTaskName()); - } - } - - public List getBookChapters(){ - return bookChapterList; - - } - - public int getCurrentChapter() { - return currentChapter; - } - - public void setCurrentChapter(int current) { - this.currentChapter = current; - } - - public int getLastChapter() { - return lastChapter; - } - - public void setLastChapter(int lastChapter) { - this.lastChapter = lastChapter; - } - - //多线程访问的问题,所以需要同步机制 - public int getStatus() { - return status; - } - - public void setStatus(int status){ - this.status = status; - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - - public synchronized void resetBookChapterList() { - bookChapterList = null; - } - - - public CollBookBean getCollBookBean() { - - List list = LitePal.where("bookId=?", bookId).find(CollBookBean.class); - if (list!=null&&list.size()>0){ - return list.get(0); - }else { - return collBookBean; - } - } - - public void setCollBookBean(CollBookBean collBookBean) { - this.collBookBean = collBookBean; - } -} diff --git a/app/src/main/java/com/novel/read/model/db/dbManage/BookManager.java b/app/src/main/java/com/novel/read/model/db/dbManage/BookManager.java deleted file mode 100644 index b28cfa2..0000000 --- a/app/src/main/java/com/novel/read/model/db/dbManage/BookManager.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.novel.read.model.db.dbManage; - -import com.novel.read.constants.Constant; -import com.novel.read.utlis.FileUtils; - -import java.io.File; -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.Map; - -public class BookManager { - private static final String TAG = "BookManager"; - private String chapterName; - private String bookId; - private long chapterLen; - private long position; - private Map cacheMap = new HashMap<>(); - private static volatile BookManager sInstance; - - public static BookManager getInstance(){ - if (sInstance == null){ - synchronized (BookManager.class){ - if (sInstance == null){ - sInstance = new BookManager(); - } - } - } - return sInstance; - } - - public boolean openChapter(String bookId, String chapterName){ - return openChapter(bookId,chapterName,0); - } - - public boolean openChapter(String bookId, String chapterName, long position){ - //如果文件不存在,则打开失败 - File file = new File(Constant.BOOK_CACHE_PATH + bookId - + File.separator + chapterName + FileUtils.SUFFIX_NB); - if (!file.exists()){ - return false; - } - this.bookId = bookId; - this.chapterName = chapterName; - this.position = position; - createCache(); - return true; - } - - private void createCache(){ - //创建Cache - if (!cacheMap.containsKey(chapterName)){ - Cache cache = new Cache(); - File file = getBookFile(bookId, chapterName); - //TODO:数据加载默认utf-8(以后会增加判断),FileUtils采用Reader获取数据的,可能用byte会更好一点 - char[] array = FileUtils.getFileContent(file).toCharArray(); - WeakReference charReference = new WeakReference<>(array); - cache.size = array.length; - cache.data = charReference; - cacheMap.put(chapterName, cache); - - chapterLen = cache.size; - } - else { - chapterLen = cacheMap.get(chapterName).getSize(); - } - } - - public void setPosition(long position){ - this.position = position; - } - - public long getPosition(){ - return position; - } - - //获取上一段 - public String getPrevPara(){ - //首先判断是否Position已经达到起始位置,已经越界 - if (position < 0){ - return null; - } - - //初始化从后向前获取的起始点,终止点,文本 - int end = (int)position; - int begin = end; - char[] array = getContent(); - - while (begin >= 0) { //判断指针是否达到章节的起始位置 - char character = array[begin]; //获取当前指针下的字符 - - //判断当前字符是否为换行,如果为换行,就代表获取到了一个段落,并退出。 - //有可能发生初始指针指的就是换行符的情况。 - if ((character+"").equals("\n") && begin != end) { - position = begin; - //当当前指针指向换行符的时候向后退一步 - begin++; - break; - } - //向前进一步 - begin--; - } - //最后end获取到段落的起始点,begin是段落的终止点。 - - //当越界的时候,保证begin在章节内 - if (begin < 0){ - begin = 0;//在章节内 - position = -1; //越界 - } - int size = end+1 - begin; - return new String(array,begin,size); - } - - //获取下一段 - public String getNextPara(){ - //首先判断是否Position已经达到终点位置 - if (position >= chapterLen){ - return null; - } - - //初始化起始点,终止点。 - int begin = (int)position; - int end = begin; - char[] array = getContent(); - - while (end < chapterLen) { //判断指针是否在章节的末尾位置 - char character = array[end]; //获取当前指针下的字符 - //判断当前字符是否为换行,如果为换行,就代表获取到了一个段落,并退出。 - //有可能发生初始指针指的就是换行符的情况。 - //这里当遇到\n的时候,不需要回退 - if ((character+"").equals("\n") && begin != end){ - ++end;//指向下一字段 - position = end; - break; - } - //指向下一字段 - end++; - } - //所要获取的字段的长度 - int size = end - begin; - return new String(array,begin,size); - } - - //获取章节的内容 - public char[] getContent() { - if (cacheMap.size() == 0){ - return new char[1]; - } - char[] block = cacheMap.get(chapterName).getData().get(); - if (block == null) { - File file = getBookFile(bookId, chapterName); - block = FileUtils.getFileContent(file).toCharArray(); - Cache cache = cacheMap.get(chapterName); - cache.data = new WeakReference(block); - } - return block; - } - - public long getChapterLen(){ - return chapterLen; - } - - public void clear(){ - cacheMap.clear(); - position = 0; - chapterLen = 0; - } - - /** - * 创建或获取存储文件 - * @param folderName 文件夹 - * @param fileName 文件 - * @return 文件 - */ - public static File getBookFile(String folderName, String fileName){ - return FileUtils.getFile(Constant.BOOK_CACHE_PATH + folderName - + File.separator + fileName + FileUtils.SUFFIX_NB); - } - - public static long getBookSize(String folderName){ - return FileUtils.getDirSize(FileUtils - .getFolder(Constant.BOOK_CACHE_PATH + folderName)); - } - - /** - * 根据文件名判断是否被缓存过 (因为可能数据库显示被缓存过,但是文件中却没有的情况,所以需要根据文件判断是否被缓存 - * 过) - * @param folderName : bookId - * @param fileName: chapterName - * @return 是否被缓存过 - */ - public static boolean isChapterCached(String folderName, String fileName){ - File file = new File(Constant.BOOK_CACHE_PATH + folderName - + File.separator + fileName + FileUtils.SUFFIX_NB); - return file.exists(); - } - - public class Cache { - private long size; - private WeakReference data; - - public WeakReference getData() { - return data; - } - - public void setData(WeakReference data) { - this.data = data; - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - } -} diff --git a/app/src/main/java/com/novel/read/model/db/dbManage/BookRepository.java b/app/src/main/java/com/novel/read/model/db/dbManage/BookRepository.java deleted file mode 100644 index dab723b..0000000 --- a/app/src/main/java/com/novel/read/model/db/dbManage/BookRepository.java +++ /dev/null @@ -1,348 +0,0 @@ -package com.novel.read.model.db.dbManage; - -import android.content.ContentValues; -import android.util.Log; - -import com.novel.read.constants.Constant; -import com.novel.read.model.db.BookChapterBean; -import com.novel.read.model.db.BookRecordBean; -import com.novel.read.model.db.BookSignTable; -import com.novel.read.model.db.ChapterInfoBean; -import com.novel.read.model.db.CollBookBean; -import com.novel.read.model.db.DownloadTaskBean; -import com.novel.read.utlis.FileUtils; -import com.novel.read.utlis.IOUtils; -import com.novel.read.utlis.SpUtil; -import com.novel.read.widget.page.Void; - -import org.litepal.LitePal; -import org.litepal.crud.callback.SaveCallback; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Reader; -import java.io.Writer; -import java.util.ArrayList; -import java.util.List; - -import io.reactivex.Single; -import io.reactivex.SingleEmitter; -import io.reactivex.SingleOnSubscribe; - -/** - * Created by newbiechen on 17-5-8. - * 存储关于书籍内容的信息(CollBook(收藏书籍),BookChapter(书籍列表),ChapterInfo(书籍章节),BookRecord(记录),BookSignTable书签) - */ - -public class BookRepository { - private static final String TAG = "CollBookManager"; - private static volatile BookRepository sInstance; - - private BookRepository() { - } - - public static BookRepository getInstance() { - if (sInstance == null) { - synchronized (BookRepository.class) { - if (sInstance == null) { - sInstance = new BookRepository(); - } - } - } - return sInstance; - } - - //存储已收藏书籍 - public void saveCollBookWithAsync(CollBookBean bean) { - - bean.saveOrUpdate("bookId=?", bean.getId()); - for (int i = 0; i < bean.getBookChapters().size(); i++) { - bean.getBookChapters().get(i).setCollBookBean(bean); - } - LitePal.saveAllAsync(bean.getBookChapters()).listen(new SaveCallback() { - @Override - public void onFinish(boolean success) { - Log.e(TAG, "saveCollBookWithAsync: " + success); - } - }); - } - - /** - * 异步存储。 - * 同时保存BookChapter - * - * @param beans - */ - public void saveCollBooksWithAsync(List beans) { -// mSession.startAsyncSession() -// .runInTx( -// () -> { -// for (CollBookBean bean : beans){ -// if (bean.getBookChapters() != null){ -// //存储BookChapterBean(需要修改,如果存在id相同的则无视) -// mSession.getBookChapterBeanDao() -// .insertOrReplaceInTx(bean.getBookChapters()); -// } -// } -// //存储CollBook (确保先后顺序,否则出错) -// mCollBookDao.insertOrReplaceInTx(beans); -// } -// ); - } - - public void saveCollBook(CollBookBean bean) { - ContentValues values = new ContentValues(); - values.put("isUpdate", bean.isUpdate()); - values.put("lastRead", bean.getLastRead()); - values.put("lastChapter", bean.getLastChapter()); - LitePal.updateAll(CollBookBean.class, values, "bookId=?", bean.getId()); - } - - public void saveCollBooks(List beans) { - for (int i = 0; i < beans.size(); i++) { - ContentValues values = new ContentValues(); - values.put("isUpdate", beans.get(i).isUpdate()); - values.put("lastRead", beans.get(i).getLastRead()); - values.put("lastChapter", beans.get(i).getLastChapter()); - values.put("updated", beans.get(i).getUpdated()); - LitePal.updateAll(CollBookBean.class, values, "bookId=?", beans.get(i).getId()); - } - } - - /** - * 异步存储BookChapter - * - * @param beans - */ - public void saveBookChaptersWithAsync(List beans, CollBookBean collBookBean) { - LitePal.deleteAll(CollBookBean.class, "bookId=?", collBookBean.getId()); - collBookBean.setBookChapters(beans); - collBookBean.saveOrUpdate("bookId=?", collBookBean.getId()); - for (int i = 0; i < collBookBean.getBookChapters().size(); i++) { - collBookBean.getBookChapters().get(i).setCollBookBean(collBookBean); - } - LitePal.saveAllAsync(collBookBean.getBookChapters()).listen(new SaveCallback() { - @Override - public void onFinish(boolean success) { - Log.e(TAG, "saveCollBookWithAsync: " + success); - } - }); - } - - /** - * 存储章节 - * - * @param folderName - * @param fileName - * @param content - */ - public void saveChapterInfo(String folderName, String fileName, String content) { - String str = content.replaceAll("\\\\n\\\\n", "\n"); - File file = BookManager.getBookFile(folderName, fileName); - //获取流并存储 - Writer writer = null; - try { - writer = new BufferedWriter(new FileWriter(file)); - writer.write(str); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - IOUtils.INSTANCE.close(writer); - } - } - - public void saveBookRecord(BookRecordBean bean) { - bean.saveOrUpdateAsync("bookId=?", bean.getBookId()).listen(new SaveCallback() { - @Override - public void onFinish(boolean success) { - Log.e("saveBookRecord", "onFinish: " + success); - } - }); - } - - /*****************************get************************************************/ - public CollBookBean getCollBook(String bookId) { - List bean = LitePal.where("bookId =?", bookId).find(CollBookBean.class); - if (bean != null && bean.size() > 0) { - return bean.get(0); - } else { - return null; - } - } - - - public List getCollBooks() { - List collBookBeans; - if (SpUtil.getBooleanValue(Constant.BookSort, false)) { //默认根据阅读时间排序 - collBookBeans = LitePal.order("updated desc").find(CollBookBean.class); - } else { - collBookBeans = LitePal.order("lastRead desc").find(CollBookBean.class); - } - return collBookBeans; - } - - - //获取书籍列表 - public List getBookChaptersInRx(String bookId) { - List bookBeans = LitePal.where("bookId=?", bookId).find(CollBookBean.class, true); - if (bookBeans != null && bookBeans.size() > 0) { - return bookBeans.get(0).getBookChapters(); - } else { - return new ArrayList<>(); - } - } - - //获取阅读记录 - public BookRecordBean getBookRecord(String bookId) { - List beans = LitePal.where("bookId=?", bookId).find(BookRecordBean.class); - if (beans != null && beans.size() > 0) { - return beans.get(0); - } else { - return null; - } - } - - //TODO:需要进行获取编码并转换的问题 - public ChapterInfoBean getChapterInfoBean(String folderName, String fileName) { - File file = new File(Constant.BOOK_CACHE_PATH + folderName - + File.separator + fileName + FileUtils.SUFFIX_NB); - if (!file.exists()) return null; - Reader reader = null; - String str = null; - StringBuilder sb = new StringBuilder(); - try { - reader = new FileReader(file); - BufferedReader br = new BufferedReader(reader); - while ((str = br.readLine()) != null) { - sb.append(str); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - IOUtils.INSTANCE.close(reader); - } - - ChapterInfoBean bean = new ChapterInfoBean(); - bean.setTitle(fileName); - bean.setBody(sb.toString()); - return bean; - } - - /************************************************************/ - /************************************************************/ - public Single deleteCollBookInRx(CollBookBean bean) { - return Single.create(new SingleOnSubscribe() { - @Override - public void subscribe(SingleEmitter e) throws Exception { - //查看文本中是否存在删除的数据 - deleteBook(bean.getId()); - //删除任务 - deleteDownloadTask(bean.getId()); - //删除目录 - deleteBookChapter(bean.getId()); - //删除CollBook - LitePal.deleteAll(CollBookBean.class, "bookId=?", bean.getId()); - e.onSuccess(new Void()); - } - }); - } - - //这个需要用rx,进行删除 - public void deleteBookChapter(String bookId) { -// mSession.getBookChapterBeanDao() -// .queryBuilder() -// .where(BookChapterBeanDao.Properties.BookId.eq(bookId)) -// .buildDelete() -// .executeDeleteWithoutDetachingEntities(); - } - - public void deleteCollBook(CollBookBean collBook) { -// mCollBookDao.delete(collBook); - } - - //删除书籍 - public void deleteBook(String bookId) { - FileUtils.deleteFile(Constant.INSTANCE.BOOK_CACHE_PATH + bookId); - } - - public void deleteBookRecord(String id) { -// mSession.getBookRecordBeanDao() -// .queryBuilder() -// .where(BookRecordBeanDao.Properties.BookId.eq(id)) -// .buildDelete() -// .executeDeleteWithoutDetachingEntities(); - } - - //删除任务 - public void deleteDownloadTask(String bookId) { -// mSession.getDownloadTaskBeanDao() -// .queryBuilder() -// .where(DownloadTaskBeanDao.Properties.BookId.eq(bookId)) -// .buildDelete() -// .executeDeleteWithoutDetachingEntities(); - } - - public List getDownloadTaskList() { - return LitePal.findAll(DownloadTaskBean.class); - } - - - public void saveDownloadTask(DownloadTaskBean bean) { - bean.saveOrUpdate("bookId=?", bean.getBookId()); - CollBookBean collBookBean = bean.getCollBookBean(); - for (int i = 0; i < bean.getBookChapters().size(); i++) { - bean.getBookChapters().get(i).setDownloadTaskBean(bean); - bean.getBookChapters().get(i).setCollBookBean(collBookBean); - bean.getBookChapters().get(i).saveOrUpdate("chapterId=?", bean.getBookChapters().get(i).getId()); - - } - } - - /** - * 获取本地书签 - * - * @param bookId 书籍Id - * @return 书签 - */ - public List getSign(String bookId) { - return LitePal.where("bookId=?", bookId).find(BookSignTable.class); - } - - /** - * 添加书签 - * - * @param bookId bookId - * @param articleId articleId - * @param content content - */ - public void addSign(String bookId, String articleId, String content) { - BookSignTable bookSignTable = new BookSignTable(bookId, articleId, content); - bookSignTable.save(); - } - - /** - * 删除书签 - * - * @param articleId articleId - */ - public void deleteSign(String articleId) { - LitePal.deleteAll(BookSignTable.class, "articleId=?", articleId); - } - - /** - * 根据id查询书签是否存在 - * - * @param articleId - */ - public boolean getSignById(String articleId) { - List mList = LitePal.where("articleId=?", articleId).find(BookSignTable.class); - return mList.size() > 0; - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/BookArticleResp.java b/app/src/main/java/com/novel/read/model/protocol/BookArticleResp.java deleted file mode 100644 index a5b0807..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/BookArticleResp.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.novel.read.model.protocol; - -import com.novel.read.model.db.BookChapterBean; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -public class BookArticleResp implements Serializable { - - - private List article; - private List bookChapterBean; - - public List getArticle() { - return article; - } - - public void setArticle(List article) { - this.article = article; - } - - public static class ArticleBean { - /** - * id : 1 - * title : 第一章 我为帝辛!【求支持】 - * words : 4272 - * create_time : 1560048488 - */ - - private int id; - private String title; - private int words; - private int create_time; - private String volume; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getTitle() { - return getVolume()+title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getWords() { - return words; - } - - public void setWords(int words) { - this.words = words; - } - - public int getCreate_time() { - return create_time; - } - - public void setCreate_time(int create_time) { - this.create_time = create_time; - } - - public String getVolume() { - return volume == null ? "" : volume; - } - - public void setVolume(String volume) { - this.volume = volume; - } - } - - public List getChapterBean() { - if (bookChapterBean == null) { - bookChapterBean = createChapterBean(); - } - return bookChapterBean; - } - - private List createChapterBean() { - List mList = new ArrayList<>(); - for (ArticleBean articleBean:getArticle()){ - mList.add(new BookChapterBean(String.valueOf(articleBean.getId()),articleBean.getTitle())); - } - return mList; - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/BookDetailResp.java b/app/src/main/java/com/novel/read/model/protocol/BookDetailResp.java deleted file mode 100644 index 981d7ca..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/BookDetailResp.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.novel.read.model.protocol; - -import com.novel.read.model.db.CollBookBean; -import com.novel.read.utlis.StringUtils; - -/** - * create by zlj on 2019/6/18 - * describe: - */ -public class BookDetailResp { - - /** - * book : {"id":1,"title":"帝逆洪荒","cover":"http://dev.duoduvip.com/uploads/20190609/408f516e930518df187acc911208f004.png","description":" 【群号536o626o6】人族至宝崆峒印逆天回到未来,带来周天附身殷商人皇纣王之身!帝辛怒吼:人族要自强,人族要自立,人族要自主!但随即帝辛现了洪荒并不是那么简单,洪荒在亘古居然被打碎了,有蛮荒,莽荒,大荒三荒并存,四荒合并成就洪荒大世界!兽皇神逆,魔祖罗T,阴阳老祖,乾坤老祖,扬眉大仙,洪荒大能纷纷出世!真龙老祖,凤凰老母,瑞麒麟三族老祖出世,镇压三族!东皇太一,妖皇帝俊设计假死,瞒过众圣,如今万妖齐聚!且看帝辛如何在万族夹击之中求生存,成就一代人族大帝,庇佑人族!
各位书友要是觉得《帝逆洪荒》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!帝逆洪荒最新章节,帝逆洪荒无弹窗,帝逆洪荒全文阅读.","hot":100,"like":20,"author":"天子辉","create_time":1560048488,"words":4637684} - * last_article : {"id":1,"title":"第一章 我为帝辛!【求支持】","create_time":1560048488} - */ - - private BookBean book; - private LastArticleBean last_article; - private CollBookBean collBookBean; - - public BookBean getBook() { - return book; - } - - public void setBook(BookBean book) { - this.book = book; - } - - public LastArticleBean getLast_article() { - return last_article; - } - - public void setLast_article(LastArticleBean last_article) { - this.last_article = last_article; - } - - public static class BookBean { - /** - * id : 1 - * title : 帝逆洪荒 - * cover : http://dev.duoduvip.com/uploads/20190609/408f516e930518df187acc911208f004.png - * description : 【群号536o626o6】人族至宝崆峒印逆天回到未来,带来周天附身殷商人皇纣王之身!帝辛怒吼:人族要自强,人族要自立,人族要自主!但随即帝辛现了洪荒并不是那么简单,洪荒在亘古居然被打碎了,有蛮荒,莽荒,大荒三荒并存,四荒合并成就洪荒大世界!兽皇神逆,魔祖罗T,阴阳老祖,乾坤老祖,扬眉大仙,洪荒大能纷纷出世!真龙老祖,凤凰老母,瑞麒麟三族老祖出世,镇压三族!东皇太一,妖皇帝俊设计假死,瞒过众圣,如今万妖齐聚!且看帝辛如何在万族夹击之中求生存,成就一代人族大帝,庇佑人族!
各位书友要是觉得《帝逆洪荒》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!帝逆洪荒最新章节,帝逆洪荒无弹窗,帝逆洪荒全文阅读. - * hot : 100 - * like : 20 - * author : 天子辉 - * create_time : 1560048488 - * words : 4637684 - */ - - private int id; - private String title; - private String cover; - private String description; - private int hot; - private int like; - private String author; - private int create_time; - private int words; - private int include_image; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getCover() { - return cover; - } - - public void setCover(String cover) { - this.cover = cover; - } - - public String getDescription() { - return description == null ? "" : StringUtils.INSTANCE.delete160(description); - } - - public void setDescription(String description) { - this.description = description; - } - - public int getHot() { - return hot; - } - - public void setHot(int hot) { - this.hot = hot; - } - - public String getLike() { - return like+"%"; - } - - public void setLike(int like) { - this.like = like; - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - - public int getCreate_time() { - return create_time; - } - - public void setCreate_time(int create_time) { - this.create_time = create_time; - } - - public int getWords() { - return words; - } - - public void setWords(int words) { - this.words = words; - } - - public int getInclude_image() { - return include_image; - } - - public void setInclude_image(int include_image) { - this.include_image = include_image; - } - } - - public static class LastArticleBean { - /** - * id : 1 - * title : 第一章 我为帝辛!【求支持】 - * create_time : 1560048488 - */ - - private int id; - private String title; - private long create_time; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getTitle() { - return title == null ? "" : title; - } - - public void setTitle(String title) { - this.title = title; - } - - public long getCreate_time() { - return create_time; - } - - public void setCreate_time(long create_time) { - this.create_time = create_time; - } - } - - public CollBookBean getCollBookBean(){ - if (collBookBean == null){ - collBookBean = createCollBookBean(); - } - return collBookBean; - } - - private CollBookBean createCollBookBean(){ - CollBookBean bean = new CollBookBean(); - bean.setId(String.valueOf(getBook().getId())); - bean.setTitle(getBook().getTitle()); - bean.setAuthor(getBook().getAuthor()); - bean.setShortIntro(getBook().getDescription()); - bean.setCover(getBook().getCover()); - bean.setInclude_image(getBook().getInclude_image()); - if (getLast_article()!=null){ - bean.setUpdated(String.valueOf(getLast_article().getCreate_time())); - bean.setLastChapter(getLast_article().getTitle()); - } - return bean; - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/CategoryTypeResp.kt b/app/src/main/java/com/novel/read/model/protocol/CategoryTypeResp.kt deleted file mode 100644 index 634bbcc..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/CategoryTypeResp.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.novel.read.model.protocol - -import com.novel.read.base.MyApp -import com.novel.read.utlis.StringUtils - -import java.io.Serializable - -class CategoryTypeResp : Serializable { - - - var category: List = arrayListOf() - - class CategoryBean { - /** - * id : 1 - * title : 玄幻奇幻 - * cover : - */ - - var id: Int = 0 - var title: String = "" - get() = StringUtils.convertCC(field) - var cover: String = "" - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/ChapterInfoPackage.kt b/app/src/main/java/com/novel/read/model/protocol/ChapterInfoPackage.kt deleted file mode 100644 index c8177f3..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/ChapterInfoPackage.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.novel.read.model.protocol - -import com.novel.read.model.db.ChapterInfoBean - -import java.io.Serializable - -class ChapterInfoPackage : Serializable { - - var article: List = ArrayList() - - -} diff --git a/app/src/main/java/com/novel/read/model/protocol/HotSearchResp.kt b/app/src/main/java/com/novel/read/model/protocol/HotSearchResp.kt deleted file mode 100644 index 4f0b97a..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/HotSearchResp.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.novel.read.model.protocol - -import java.io.Serializable -import java.util.ArrayList - -class HotSearchResp : Serializable { - - - var key: List = arrayListOf() -} diff --git a/app/src/main/java/com/novel/read/model/protocol/MarkResp.kt b/app/src/main/java/com/novel/read/model/protocol/MarkResp.kt deleted file mode 100644 index b9a0209..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/MarkResp.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.novel.read.model.protocol - -class MarkResp { - - var sign: List? = null - - class SignBean { - /** - * id : 8 - * uid : 1 - * book_id : 1 - * article_id : 1 - * words : 0 - * create_time : 1561450031 - * update_time : 1561450031 - * delete_time : null - */ - - var id: Int = 0 - var uid: Int = 0 - var book_id: Int = 0 - var article_id: Int = 0 - var words: Int = 0 - var create_time: Int = 0 - var update_time: Int = 0 - var delete_time: Any? = null - var content: String? = null - get() = if (field == null) "" else field - var isEdit: Boolean = false - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/RankByUpdateResp.java b/app/src/main/java/com/novel/read/model/protocol/RankByUpdateResp.java deleted file mode 100644 index 43aee00..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/RankByUpdateResp.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.novel.read.model.protocol; - -import com.novel.read.base.MyApp; -import com.novel.read.utlis.StringUtils; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -/** - * create by zlj on 2019/6/20 - * describe: - */ -public class RankByUpdateResp implements Serializable { - - - private List book; - - public List getBook() { - if (book == null) { - return new ArrayList<>(); - } - return book; - } - - public void setBook(List book) { - this.book = book; - } - - public static class BookBean { - /** - * id : 2446 - * title : 吞天记 - * cover : http://api.duoduvip.com/uploads/nocover.jpg - * description :     炎黄古域,浩瀚无边,无尽岁月中诞生诸多太古仙妖,撕裂天地,脱三界五行。更有万物神灵,天生神体,穿梭虚空,逆乱阴阳,无所不能。
    当今乃仙道盛世,万法通天,众生修道,妖孽横行!
    东吴太子吴煜,于绝境中得东方绝世战仙之衣钵,自此横空出世,逆天崛起。
    亿万世人心中,他是普渡众生的帝仙!
    漫天仙佛眼中,他是吞噬天地的妖魔!
    ……
    想和作者探讨剧情,加入讨论群,请加我微信:fengqingyang17k。或搜风青阳。
    各位书友要是觉得《吞天记》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!吞天记最新章节,吞天记无弹窗,吞天记全文阅读. - * hot : 41926 - * like : 65 - * author : 风青阳 - * create_time : 1561001132 - */ - - private int id; - private String title; - private String cover; - private String description; - private int hot; - private int like; - private String author; - private int create_time; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getTitle() { - return title == null ? "" : StringUtils.INSTANCE.convertCC(title); - } - - public void setTitle(String title) { - this.title = title; - } - - public String getCover() { - return cover; - } - - public void setCover(String cover) { - this.cover = cover; - } - - public String getDescription() { - return description == null ? "" : StringUtils.INSTANCE.convertCC(StringUtils.INSTANCE.delete160(description)); - } - - public void setDescription(String description) { - this.description = description; - } - - public int getHot() { - return hot; - } - - public void setHot(int hot) { - this.hot = hot; - } - - public int getLike() { - return like; - } - - public void setLike(int like) { - this.like = like; - } - - public String getAuthor() { - return author == null ? "" : StringUtils.INSTANCE.convertCC(author); - } - - public void setAuthor(String author) { - this.author = author; - } - - public int getCreate_time() { - return create_time; - } - - public void setCreate_time(int create_time) { - this.create_time = create_time; - } - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/RecommendBookResp.kt b/app/src/main/java/com/novel/read/model/protocol/RecommendBookResp.kt deleted file mode 100644 index ac4d6cf..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/RecommendBookResp.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.novel.read.model.protocol - - -import com.novel.read.base.MyApp -import com.novel.read.utlis.StringUtils - -import java.io.Serializable -import java.util.ArrayList - -/** - * create by zlj on 2019/6/18 - * describe: - */ -class RecommendBookResp : Serializable { - - - var book: List = ArrayList() - - class BookBean { - /** - * id : 139 - * title : 丹师剑宗 - * cover : http://dev.duoduvip.com/uploads/20190611/b81d831d3310041846444dacca57cef9.png - * description : - * hot : 83146 - * like : 64 - * author : 伯爵 - * create_time : 1560191131 - */ - - var id: Int = 0 - var title: String? = null - get() = if (field == null) "" else StringUtils.convertCC(field!!) - var cover: String? = null - var description: String? = null - get() = if (field == null) "" else StringUtils.convertCC(StringUtils.delete160(field!!)) - var hot: Int = 0 - var like: Int = 0 - var author: String? = null - get() = if (field == null) "" else StringUtils.convertCC(field!!) - var create_time: Int = 0 - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/RecommendListResp.kt b/app/src/main/java/com/novel/read/model/protocol/RecommendListResp.kt deleted file mode 100644 index e7b808c..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/RecommendListResp.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.novel.read.model.protocol - -import com.novel.read.base.MyApp -import com.novel.read.utlis.StringUtils - -import java.io.Serializable -import java.util.ArrayList - -/** - * create by zlj on 2019/6/19 - * describe: - */ -class RecommendListResp : Serializable { - - - var list: List = ArrayList() - - class ListBean { - /** - * id : 5 - * book_id : 10 - * type : 1 - * gender : 1 - * sort : 10 - * create_time : 1560129944 - * update_time : 1560129944 - * delete_time : null - * book_title : 重生之末世宝典 - * book_cover : http://api.duoduvip.com/uploads/nocover.jpg - */ - - var id: Int = 0 - var book_id: Int = 0 - var type: Int = 0 - var gender: Int = 0 - var sort: Int = 0 - var create_time: Int = 0 - var update_time: Int = 0 - var delete_time: Any? = null - var book_title: String? = null - get() = if (field == null) "" else StringUtils.convertCC(field!!) - var book_cover: String? = null - get() = if (field == null) "" else field - var author: String? = null - get() = if (field == null) "" else StringUtils.convertCC(field!!) - var description: String? = null - get() = if (field == null) "" else StringUtils.convertCC(StringUtils.delete160(field!!)) - private var hot: Int = 0 - private var like: Int = 0 - - fun getHot(): String { - return hot.toString() - } - - fun setHot(hot: Int) { - this.hot = hot - } - - fun getLike(): String { - return "$like%" - } - - fun setLike(like: Int) { - this.like = like - } - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/SearchResp.kt b/app/src/main/java/com/novel/read/model/protocol/SearchResp.kt deleted file mode 100644 index 3065eb3..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/SearchResp.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.novel.read.model.protocol - -import com.novel.read.utlis.StringUtils - -import java.io.Serializable -import java.util.ArrayList - -class SearchResp : Serializable { - - - /** - * book : [{"id":92,"title":"极品全能狂少","category_id":3,"cover":"http://dev.duoduvip.com/uploads/20190610/64512ebcd7e79923bc59ffc1d3afb597.png","description":"","hot":99437,"like":41,"author":"一支烟的快感","create_time":1560159443},{"id":170,"title":"抬棺匠","category_id":1,"cover":"http://dev.duoduvip.com/uploads/20190611/19cfa1ff96a4bec66b14e16b997ffba9.png","description":"","hot":99184,"like":54,"author":"陈八仙","create_time":1560205316},{"id":47,"title":"都市超级医圣","category_id":3,"cover":"http://dev.duoduvip.com/uploads/20190610/868b69fdf75dc303ca70fef4ccc8bc87.png","description":" 财法侣地,修行其实是一项非常耗钱的奢侈运动。无意中得到传说中道教学家,炼丹家,医药家葛洪的传承,淳朴的山里人葛东旭开始努力赚钱。当大多数人还在读高中时,他为了炼丹修行已经开始一边读书一边赚钱。当大多数人读大学还在为交女朋友的开销愁时,他已经是一名老板。当大多数人大学毕业在为找工作四处投简历,当富二代开着跑车,在美女面前炫耀时,他已经是一名级富一代,当然还是一名大隐隐于市的至尊医圣。

各位书友要是觉得《都市超级医圣》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!都市超级医圣最新章节,都市超级医圣无弹窗,都市超级医圣全文阅读.","hot":99056,"like":21,"author":"断桥残雪","create_time":1560129236},{"id":105,"title":"神医高手在都市","category_id":3,"cover":"http://dev.duoduvip.com/uploads/20190610/ccab1913cdaf9aee2cfeaef84910a700.png","description":"","hot":98517,"like":28,"author":"复仇","create_time":1560167691},{"id":21,"title":"夜少的心尖宝贝","category_id":1,"cover":"http://dev.duoduvip.com/uploads/20190609/c0f5c2ff7fa532e4bd399625e5edacf3.png","description":" 简介:

为了能正大光明的睡乔小姐,夜少不惜采用了108种追妻方式结果有两个小萝卜头冒了出来,对他说,\u201c帅叔叔,你想追我妈咪吗我们可以教你哦\u201d夜少掀桌,\u201c叫什么叔叔乖,叫爹地\u201d夜少一生最风光得意的事情就是睡了乔小姐,留下了种,让她给自己生了两个宝贝儿子,在她的身上永恒的打上了自己的烙印夜少漫漫追妻之旅,甜甜甜,超甜宠文,1v1

各位书友要是觉得《夜少的心尖宝贝》还不错的话请不要忘记向您QQ群和微博里的朋友推荐哦!夜少的心尖宝贝最新章节,夜少的心尖宝贝无弹窗,夜少的心尖宝贝全文阅读.","hot":97296,"like":64,"author":"慕欢颜","create_time":1560095819},{"id":102,"title":"万域灵神","category_id":4,"cover":"http://dev.duoduvip.com/uploads/20190610/847703596e2af187311bb9f5ab7c26b4.png","description":"","hot":97157,"like":56,"author":"乾多多","create_time":1560165904},{"id":87,"title":"至尊剑皇","category_id":4,"cover":"http://dev.duoduvip.com/uploads/20190610/711a87b0df3446885739845fba33fce4.png","description":"","hot":96684,"like":23,"author":"半步沧桑","create_time":1560156886},{"id":166,"title":"生活在美利坚的森林游侠","category_id":3,"cover":"http://dev.duoduvip.com/uploads/20190611/d663219f43a8bbba3d0718637fa0724a.png","description":"","hot":96427,"like":77,"author":"酱疙瘩","create_time":1560203771},{"id":171,"title":"三界红包群","category_id":3,"cover":"http://dev.duoduvip.com/uploads/nocover.jpg","description":"","hot":96362,"like":47,"author":"小教主","create_time":1560205922},{"id":131,"title":"命之途","category_id":2,"cover":"http://dev.duoduvip.com/uploads/nocover.jpg","description":"","hot":94835,"like":27,"author":"莫若梦兮","create_time":1560186387}] - * limit : 10 - */ - - var limit: Int = 0 - var book: List = ArrayList() - - class BookBean { - /** - * id : 92 - * title : 极品全能狂少 - * category_id : 3 - * cover : http://dev.duoduvip.com/uploads/20190610/64512ebcd7e79923bc59ffc1d3afb597.png - * description : - * hot : 99437 - * like : 41 - * author : 一支烟的快感 - * create_time : 1560159443 - */ - - var id: Int = 0 - var title: String? = null - get() = if (field == null) "" else StringUtils.convertCC(field!!) - var category_id: Int = 0 - var cover: String? = null - get() = if (field == null) "" else field - var description: String? = null - get() = if (field == null) "" else StringUtils.convertCC(StringUtils.delete160(field!!)) - var hot: Int = 0 - var like: Int = 0 - var author: String? = null - get() = if (field == null) "" else StringUtils.convertCC(field!!) - var create_time: Int = 0 - } -} diff --git a/app/src/main/java/com/novel/read/model/protocol/UidResp.kt b/app/src/main/java/com/novel/read/model/protocol/UidResp.kt deleted file mode 100644 index 32c6468..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/UidResp.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.novel.read.model.protocol - -class UidResp { - - /** - * uid : 3 - */ - - var uid: Int = 0 -} diff --git a/app/src/main/java/com/novel/read/model/protocol/VersionResp.kt b/app/src/main/java/com/novel/read/model/protocol/VersionResp.kt deleted file mode 100644 index aa214d7..0000000 --- a/app/src/main/java/com/novel/read/model/protocol/VersionResp.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.novel.read.model.protocol - -/** - * create by 赵利君 on 2019/6/25 - * describe: - */ -class VersionResp { - - /** - * version : {"id":2,"version":"1.0.0","size":"5096","content":"正式上线","download":"https://play.google.com/store/apps/details?id=com.freebook.bookreader","coerce":1,"status":1,"push_time":1560060702,"create_time":1560060772,"update_time":1560752842,"delete_time":null} - */ - - var version: VersionBean = VersionBean() - - class VersionBean { - /** - * id : 2 - * version : 1.0.0 - * size : 5096 - * content : 正式上线 - * download : https://play.google.com/store/apps/details?id=com.freebook.bookreader - * coerce : 1 - * status : 1 - * push_time : 1560060702 - * create_time : 1560060772 - * update_time : 1560752842 - * delete_time : null - */ - - var id: Int = 0 - var version: String? = null - var size: String? = null - var content: String? = null - var download: String? = null - var coerce: Int = 0 - var status: Int = 0 - var push_time: Int = 0 - var create_time: Int = 0 - var update_time: Int = 0 - var delete_time: Any? = null - } -} diff --git a/app/src/main/java/com/novel/read/network/ServiceCreator.kt b/app/src/main/java/com/novel/read/network/ServiceCreator.kt new file mode 100644 index 0000000..97a8828 --- /dev/null +++ b/app/src/main/java/com/novel/read/network/ServiceCreator.kt @@ -0,0 +1,143 @@ +package com.novel.read.network + +import android.util.Log +import com.novel.read.network.api.BookService +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okio.Buffer +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory +import java.io.IOException +import java.net.URLEncoder +import java.nio.charset.Charset +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.jvm.Throws + + +object ServiceCreator { + + private const val BASE_URL = "http://172.104.61.64:8009/" + + private val httpClient = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .addInterceptor(LoggingInterceptor()) + .addInterceptor(HeaderInterceptor()) + + private val builder = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(httpClient.build()) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + + + private val retrofit = builder.build() + + fun create(serviceClass: Class): T = retrofit.create(serviceClass) + + /**ApiService*/ + val apiService: BookService = retrofit.create(BookService::class.java) + + private val builder1 = Retrofit.Builder() + .baseUrl("http://yijianda8.com/") + .client(httpClient.build()) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + + private val retrofit1 = builder1.build() + val apiService1: BookService = retrofit1.create(BookService::class.java) + + class LoggingInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val t1 = System.nanoTime() + Log.e(TAG, "request: ${request.url()} \n ${getRequestInfo(request)}") + + val response = chain.proceed(request) + + val t2 = System.nanoTime() + Log.e( + TAG, + "response for ${response.request() + .url()} in ${(t2 - t1) / 1e6} ms\n${getResponseInfo(response)}" + ) + return response + } + + /** + * 打印返回消息 + * @param response 返回的对象 + */ + private fun getResponseInfo(response: Response?): String? { + var str = "" + if (response == null || !response.isSuccessful) { + return str + } + val responseBody = response.body() + val contentLength = responseBody!!.contentLength() + val source = responseBody.source() + try { + source.request(Long.MAX_VALUE) // Buffer the entire body. + } catch (e: IOException) { + e.printStackTrace() + } + val buffer: Buffer = source.buffer + val charset: Charset = Charset.forName("utf-8") + if (contentLength != 0L) { + str = buffer.clone().readString(charset) + } + return str + } + + /** + * 打印请求体 + * @param request 请求的对象 + */ + private fun getRequestInfo(request: Request?): String? { + val requestBody = request!!.body() + val buffer = Buffer() + try { + if(requestBody!=null) { + requestBody.writeTo(buffer) + }else{ + return "" + } + } catch (e: IOException) { + e.printStackTrace() + return "" + } + + var charset = Charset.forName("UTF-8") + val contentType = requestBody!!.contentType() + if (contentType != null) { + charset = contentType.charset(Charset.forName("UTF-8")) + } + + return buffer.readString(charset) + } + + companion object { + const val TAG = "LoggingInterceptor" + } + } + + class HeaderInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val original = chain.request() + val request = original.newBuilder().apply { + header("model", "Android") + header("If-Modified-Since", URLEncoder.encode("${Date()}", "utf-8")) + header("User-Agent", System.getProperty("http.agent") ?: "unknown") + }.build() + return chain.proceed(request) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/network/api/ApiException.kt b/app/src/main/java/com/novel/read/network/api/ApiException.kt new file mode 100644 index 0000000..116a6fe --- /dev/null +++ b/app/src/main/java/com/novel/read/network/api/ApiException.kt @@ -0,0 +1,3 @@ +package com.novel.read.network.api + +class ApiException(var code: Int, override var message: String) : RuntimeException() \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/network/api/ApiResult.kt b/app/src/main/java/com/novel/read/network/api/ApiResult.kt new file mode 100644 index 0000000..67eefea --- /dev/null +++ b/app/src/main/java/com/novel/read/network/api/ApiResult.kt @@ -0,0 +1,18 @@ +package com.novel.read.network.api + +import androidx.annotation.Keep + +@Keep +data class ApiResult( + val code: Int, + val message: String, + private val data: T? +) { + fun apiData(): T { + if (code == 200 && data != null) { + return data + } else { + throw ApiException(code, message) + } + } +} diff --git a/app/src/main/java/com/novel/read/network/api/BookService.kt b/app/src/main/java/com/novel/read/network/api/BookService.kt new file mode 100644 index 0000000..2f08735 --- /dev/null +++ b/app/src/main/java/com/novel/read/network/api/BookService.kt @@ -0,0 +1,60 @@ +package com.novel.read.network.api + +import com.novel.read.data.model.* +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.http.* + +interface BookService { + + @POST("api/book/bookSearch") + suspend fun searchBook( + @Body body: RequestBody + ): ApiResult> + + @POST("api/book/searchTermsList") + suspend fun hotKey(): ApiResult> + + @POST("api/book/bookHome") + suspend fun getHomeInfo(@Body body: RequestBody): ApiResult + + @POST("api/book/bookDetail") + suspend fun getBookInfo(@Body body: RequestBody):ApiResult + + @POST("api/book/bookChapterList") + suspend fun getDirectory(@Body body: RequestBody):ApiResult + + @POST("api/book/bookChapterDetail") + suspend fun getChapterContent(@Body body: RequestBody):ApiResult + + @POST("api/book/typeList") + suspend fun getChannelList(): ApiResult + + @POST("api/book/BookList") + suspend fun getBookList(@Body body: RequestBody): ApiResult> + + @POST("api/book/bookRankQuery") + suspend fun getBooRank(@Body body: RequestBody): ApiResult> + + @POST("api/book/similarRecommend") + suspend fun getSimilarRecommend(@Body body: RequestBody): ApiResult> + + @POST("api/book/feedback") + suspend fun feedback(@Body body: RequestBody): ApiResult + + @POST("api/edition/editionUpdate") + suspend fun appUpdate(@Body body: RequestBody): ApiResult + + @POST("api/user/googleLogin") + suspend fun googleLogin(@Body loginReq: LoginReq): ApiResult + + @POST("api/order/getCommodityList") + suspend fun getGoods(): ApiResult> + + @POST("api/order/addBookOrder") + suspend fun buyVip(): ApiResult> + + @Streaming + @GET() + suspend fun getImage(@Url fileUrl: String): ResponseBody +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/network/repository/BookRepository.kt b/app/src/main/java/com/novel/read/network/repository/BookRepository.kt new file mode 100644 index 0000000..744094f --- /dev/null +++ b/app/src/main/java/com/novel/read/network/repository/BookRepository.kt @@ -0,0 +1,126 @@ +package com.novel.read.network.repository + +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.data.model.ChapterContentResp +import com.novel.read.data.model.LoginReq +import com.novel.read.network.ServiceCreator +import com.novel.read.utils.ext.GSON +import com.novel.read.help.coroutine.Coroutine +import kotlinx.coroutines.* +import okhttp3.MediaType +import okhttp3.RequestBody +import kotlin.coroutines.CoroutineContext + +class BookRepository { + + suspend fun getChannelList() = withContext(Dispatchers.IO) { + ServiceCreator.apiService.getChannelList().apiData() + } + + suspend fun getBookInfo(bookId: Long) = withContext(Dispatchers.IO) { + val map = HashMap() + map["bookId"] = bookId + ServiceCreator.apiService.getBookInfo(mapToBody(map)).apiData() + } + + suspend fun getDirectory(bookId: Long) = withContext(Dispatchers.IO) { + val map = HashMap() + map["bookId"] = bookId + ServiceCreator.apiService.getDirectory(mapToBody(map)).apiData() + } + + + fun getContents( + scope: CoroutineScope, + book: Book, + bookChapter: BookChapter, + context: CoroutineContext = Dispatchers.IO + ): Coroutine { + return Coroutine.async(scope, context) { + val map = HashMap() + map["bookId"] = book.bookId + map["chapterId"] = bookChapter.chapterId + getChapterContent(book.bookId, bookChapter.chapterId) + } + } + + private suspend fun getChapterContent(bookId: Long, chapterId: Long): ChapterContentResp { + val map = HashMap() + map["bookId"] = bookId + map["chapterId"] = chapterId + return ServiceCreator.apiService.getChapterContent(mapToBody(map)).apiData() + } + + + suspend fun getBookList(channelId: Long, pageNum: Int, pageSize: Int) = + withContext(Dispatchers.IO) { + val map = HashMap() + map["bookTypeId"] = channelId.toString() + map["pageNum"] = pageNum.toString() + map["pageSize"] = pageSize.toString() + ServiceCreator.apiService.getBookList(mapToBody(map)).apiData() + } + + suspend fun getDailyList(pageNum: Int, pageSize: Int) = + withContext(Dispatchers.IO) { + val map = HashMap() + map["hotStatus"] = "1" + map["pageNum"] = pageNum.toString() + map["pageSize"] = pageSize.toString() + ServiceCreator.apiService.getBookList(mapToBody(map)).apiData() + } + + suspend fun getEndList(pageNum: Int, pageSize: Int) = + withContext(Dispatchers.IO) { + val map = HashMap() + map["endStatus"] = "1" + map["pageNum"] = pageNum.toString() + map["pageSize"] = pageSize.toString() + ServiceCreator.apiService.getBookList(mapToBody(map)).apiData() + } + + + suspend fun getBooRank(pageNum: Int, pageSize: Int, rankType: Int) = + withContext(Dispatchers.IO) { + val map = HashMap() + map["rankType"] = rankType.toString() + map["pageNum"] = pageNum.toString() + map["pageSize"] = pageSize.toString() + ServiceCreator.apiService.getBooRank(mapToBody(map)).apiData() + } + + suspend fun getSimilarRecommend(bookTypeId: Int) = + withContext(Dispatchers.IO) { + val map = HashMap() + map["bookTypeId"] = bookTypeId + ServiceCreator.apiService.getSimilarRecommend(mapToBody(map)).apiData() + } + + suspend fun feedback(content: String) = + withContext(Dispatchers.IO) { + val map = HashMap() + map["content"] = content + ServiceCreator.apiService.feedback(mapToBody(map)).apiData() + } + + suspend fun login(loginReq: LoginReq) = withContext(Dispatchers.IO) { + ServiceCreator.apiService.googleLogin(loginReq).apiData() + } + + suspend fun getGoods() = withContext(Dispatchers.IO) { + ServiceCreator.apiService.getGoods().apiData() + } + + suspend fun buyVip() = withContext(Dispatchers.IO) { + ServiceCreator.apiService.buyVip().apiData() + } + + private fun mapToBody(map: HashMap): RequestBody { + return RequestBody.create( + MediaType.parse("application/json;charset=UTF-8"), + GSON.toJson(map) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/network/repository/HomeRepository.kt b/app/src/main/java/com/novel/read/network/repository/HomeRepository.kt new file mode 100644 index 0000000..544f5f9 --- /dev/null +++ b/app/src/main/java/com/novel/read/network/repository/HomeRepository.kt @@ -0,0 +1,35 @@ +package com.novel.read.network.repository + +import com.novel.read.App +import com.novel.read.constant.AppConst +import com.novel.read.network.ServiceCreator +import com.novel.read.utils.ext.GSON +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType +import okhttp3.RequestBody + +class HomeRepository { + + + suspend fun getHomeBook(bookClass: Int) = withContext(Dispatchers.IO) { + val map = HashMap() + val books = ServiceCreator.apiService.getHomeInfo(mapToBody(map)).apiData() + books + } + + suspend fun appUpdate() = withContext(Dispatchers.IO) { + val map = HashMap() + map["shellName"] = AppConst.shellName + map["editionCode"] = App.versionName + ServiceCreator.apiService.appUpdate(mapToBody(map)).apiData() + } + + private fun mapToBody(map: HashMap): RequestBody { + return RequestBody.create( + MediaType.parse("application/json;charset=UTF-8"), + GSON.toJson(map) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/network/repository/ImageRepository.kt b/app/src/main/java/com/novel/read/network/repository/ImageRepository.kt new file mode 100644 index 0000000..b1677a4 --- /dev/null +++ b/app/src/main/java/com/novel/read/network/repository/ImageRepository.kt @@ -0,0 +1,12 @@ +package com.novel.read.network.repository + +import com.novel.read.network.ServiceCreator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ImageRepository { + + suspend fun getImage(src: String) = withContext(Dispatchers.IO) { + ServiceCreator.apiService1.getImage(src) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/network/repository/SearchRepository.kt b/app/src/main/java/com/novel/read/network/repository/SearchRepository.kt new file mode 100644 index 0000000..498b9a6 --- /dev/null +++ b/app/src/main/java/com/novel/read/network/repository/SearchRepository.kt @@ -0,0 +1,33 @@ +package com.novel.read.network.repository + +import com.hankcs.hanlp.HanLP +import com.novel.read.network.ServiceCreator +import com.novel.read.utils.ext.GSON +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType +import okhttp3.RequestBody + +class SearchRepository { + + + + suspend fun searchBook(page: Int, pageSize: Int, key: String) = withContext(Dispatchers.IO) { + val map = HashMap() + val key1 = HanLP.convertToSimplifiedChinese(key) + map["pageNum"] = page.toString() + map["pageSize"] = pageSize.toString() + map["searchTerms"] = key1 + val body: RequestBody = + RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), GSON.toJson(map)) + val books = ServiceCreator.apiService.searchBook(body).apiData() + books + } + + suspend fun hotKey() = withContext(Dispatchers.IO) { + val books = ServiceCreator.apiService.hotKey().apiData() + books + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/receiver/MediaButtonReceiver.kt b/app/src/main/java/com/novel/read/receiver/MediaButtonReceiver.kt new file mode 100644 index 0000000..c3d5060 --- /dev/null +++ b/app/src/main/java/com/novel/read/receiver/MediaButtonReceiver.kt @@ -0,0 +1,92 @@ +package com.novel.read.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.view.KeyEvent +import com.novel.read.App +import com.novel.read.constant.EventBus +import com.novel.read.data.db.entity.Book +import com.novel.read.help.ActivityHelp +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.help.ReadAloud +import com.novel.read.ui.MainActivity +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.utils.ext.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +/** + * Created by zlj on 2019/1/6. + * 监听耳机键 + */ + +class MediaButtonReceiver : BroadcastReceiver() { + + companion object { + + fun handleIntent(context: Context, intent: Intent): Boolean { + val intentAction = intent.action + if (Intent.ACTION_MEDIA_BUTTON == intentAction) { + val keyEvent = + intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) ?: return false + val keycode: Int = keyEvent.keyCode + val action: Int = keyEvent.action + if (action == KeyEvent.ACTION_DOWN) { + when (keycode) { + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + } + KeyEvent.KEYCODE_MEDIA_NEXT -> { + } + else -> readAloud(context) + } + } + } + return true + } + + private fun readAloud(context: Context) { + when { + BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) { + ReadAloud.pause(context) + } else { + ReadAloud.resume(context) + } + + ActivityHelp.isExist(ReadBookActivity::class.java) -> + postEvent(EventBus.MEDIA_BUTTON, true) + else -> if (context.getPrefBoolean("mediaButtonOnExit", true)) { + GlobalScope.launch(Main) { + val lastBook: Book? = withContext(IO) { + App.db.getBookDao().lastReadBook() + } + lastBook?.let { + if (!ActivityHelp.isExist(MainActivity::class.java)) { + Intent(context, MainActivity::class.java).let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(it) + } + } + Intent(context, ReadBookActivity::class.java).let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + it.putExtra("readAloud", true) + context.startActivity(it) + } + } + } + } + } + } + } + + override fun onReceive(context: Context, intent: Intent) { + if (handleIntent(context, intent) && isOrderedBroadcast) { + abortBroadcast() + } + } + +} diff --git a/app/src/main/java/com/novel/read/service/BaseReadAloudService.kt b/app/src/main/java/com/novel/read/service/BaseReadAloudService.kt new file mode 100644 index 0000000..dd55ab7 --- /dev/null +++ b/app/src/main/java/com/novel/read/service/BaseReadAloudService.kt @@ -0,0 +1,344 @@ +package com.novel.read.service + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.BitmapFactory +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Handler +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import androidx.annotation.CallSuper +import androidx.core.app.NotificationCompat +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseService +import com.novel.read.constant.* +import com.novel.read.data.read.TextChapter +import com.novel.read.help.IntentDataHelp +import com.novel.read.help.IntentHelp +import com.novel.read.help.MediaHelp +import com.novel.read.receiver.MediaButtonReceiver +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.utils.ext.* + + +abstract class BaseReadAloudService : BaseService(), + AudioManager.OnAudioFocusChangeListener { + + companion object { + var isRun = false + var timeMinute: Int = 0 + var pause = true + + fun isPlay(): Boolean { + return isRun && !pause + } + } + + internal val handler = Handler() + private lateinit var audioManager: AudioManager + private var mFocusRequest: AudioFocusRequest? = null + private var broadcastReceiver: BroadcastReceiver? = null + private lateinit var mediaSessionCompat: MediaSessionCompat + private var title: String = "" + private var subtitle: String = "" + internal val contentList = arrayListOf() + internal var nowSpeak: Int = 0 + internal var readAloudNumber: Int = 0 + internal var textChapter: TextChapter? = null + internal var pageIndex = 0 + private val dsRunnable: Runnable = Runnable { doDs() } + + override fun onCreate() { + super.onCreate() + isRun = true + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + mFocusRequest = MediaHelp.getFocusRequest(this) + mediaSessionCompat = MediaSessionCompat(this, "readAloud") + initMediaSession() + initBroadcastReceiver() + upNotification() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + } + + override fun onDestroy() { + super.onDestroy() + isRun = false + pause = true + unregisterReceiver(broadcastReceiver) + postEvent(EventBus.ALOUD_STATE, Status.STOP) + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) + mediaSessionCompat.release() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.action?.let { action -> + when (action) { + IntentAction.play -> { + title = intent.getStringExtra("title") ?: "" + subtitle = intent.getStringExtra("subtitle") ?: "" + pageIndex = intent.getIntExtra("pageIndex", 0) + newReadAloud( + intent.getStringExtra("dataKey"), + intent.getBooleanExtra("play", true) + ) + } + IntentAction.pause -> pauseReadAloud(true) + IntentAction.resume -> resumeReadAloud() + IntentAction.upTtsSpeechRate -> upSpeechRate(true) + IntentAction.prevParagraph -> prevP() + IntentAction.nextParagraph -> nextP() + IntentAction.addTimer -> addTimer() + IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) + else -> stopSelf() + } + } + return super.onStartCommand(intent, flags, startId) + } + + @CallSuper + open fun newReadAloud(dataKey: String?, play: Boolean) { + dataKey?.let { + textChapter = IntentDataHelp.getData(dataKey) + textChapter?.let { textChapter -> + nowSpeak = 0 + readAloudNumber = textChapter.getReadLength(pageIndex) + contentList.clear() + if (getPrefBoolean(PreferKey.readAloudByPage)) { + for (index in pageIndex..textChapter.lastIndex) { + textChapter.page(index)?.text?.split("\n")?.let { + contentList.addAll(it) + } + } + } else { + textChapter.getUnRead(pageIndex).split("\n").forEach { + if (it.isNotEmpty()) { + contentList.add(it) + } + } + } + if (play) play() + } ?: stopSelf() + } ?: stopSelf() + } + + open fun play() { + pause = false + upNotification() + postEvent(EventBus.ALOUD_STATE, Status.PLAY) + } + + @CallSuper + open fun pauseReadAloud(pause: Boolean) { + BaseReadAloudService.pause = pause + upNotification() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) + postEvent(EventBus.ALOUD_STATE, Status.PAUSE) + } + + @CallSuper + open fun resumeReadAloud() { + pause = false + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + if (timeMinute > 1) { + doDs() + } + } + + abstract fun upSpeechRate(reset: Boolean = false) + + abstract fun prevP() + + abstract fun nextP() + + private fun setTimer(minute: Int) { + timeMinute = minute + if (minute > 0) { + handler.removeCallbacks(dsRunnable) + handler.postDelayed(dsRunnable, 60000) + } + upNotification() + } + + private fun addTimer() { + if (timeMinute == 180) { + timeMinute = 0 + handler.removeCallbacks(dsRunnable) + } else { + timeMinute += 10 + if (timeMinute > 180) timeMinute = 180 + handler.removeCallbacks(dsRunnable) + handler.postDelayed(dsRunnable, 60000) + } + postEvent(EventBus.TTS_DS, timeMinute) + upNotification() + } + + /** + * 定时 + */ + private fun doDs() { + if (!pause) { + timeMinute-- + if (timeMinute == 0) { + stopSelf() + } else if (timeMinute > 0) { + handler.postDelayed(dsRunnable, 60000) + } + } + postEvent(EventBus.TTS_DS, timeMinute) + upNotification() + } + + /** + * @return 音频焦点 + */ + fun requestFocus(): Boolean { + return MediaHelp.requestFocus(audioManager, this, mFocusRequest) + } + + /** + * 更新媒体状态 + */ + private fun upMediaSessionPlaybackState(state: Int) { + mediaSessionCompat.setPlaybackState( + PlaybackStateCompat.Builder() + .setActions(MediaHelp.MEDIA_SESSION_ACTIONS) + .setState(state, nowSpeak.toLong(), 1f) + .build() + ) + } + + /** + * 初始化MediaSession, 注册多媒体按钮 + */ + private fun initMediaSession() { + mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { + override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { + return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent) + } + }) + mediaSessionCompat.setMediaButtonReceiver( + PendingIntent.getBroadcast( + this, + 0, + Intent( + Intent.ACTION_MEDIA_BUTTON, + null, + App.INSTANCE, + MediaButtonReceiver::class.java + ), + PendingIntent.FLAG_CANCEL_CURRENT + ) + ) + mediaSessionCompat.isActive = true + } + + /** + * 断开耳机监听 + */ + private fun initBroadcastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) { + pauseReadAloud(true) + } + } + } + val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + registerReceiver(broadcastReceiver, intentFilter) + } + + /** + * 音频焦点变化 + */ + override fun onAudioFocusChange(focusChange: Int) { + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + // 重新获得焦点, 可做恢复播放,恢复后台音量的操作 + if (!pause) resumeReadAloud() + } + AudioManager.AUDIOFOCUS_LOSS -> { + // 永久丢失焦点除非重新主动获取,这种情况是被其他播放器抢去了焦点, 为避免与其他播放器混音,可将音乐暂停 + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // 暂时丢失焦点,这种情况是被其他应用申请了短暂的焦点,可压低后台音量 + if (!pause) pauseReadAloud(false) + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音), + } + } + } + + /** + * 更新通知 + */ + private fun upNotification() { + var nTitle: String = when { + pause -> getString(R.string.read_aloud_pause) + timeMinute in 1..180 -> getString( + R.string.read_aloud_timer, + timeMinute + ) + else -> getString(R.string.read_aloud_t) + } + nTitle += ": $title" + var nSubtitle = subtitle + if (subtitle.isEmpty()) + nSubtitle = getString(R.string.read_aloud_s) + val builder = NotificationCompat.Builder(this, AppConst.channelIdReadAloud) + .setSmallIcon(R.drawable.ic_volume_up) + .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.icon_read_book)) + .setOngoing(true) + .setContentTitle(nTitle) + .setContentText(nSubtitle) + .setContentIntent( + IntentHelp.activityPendingIntent(this, "activity") + ) + if (pause) { + builder.addAction( + R.drawable.ic_play_24dp, + getString(R.string.resume), + aloudServicePendingIntent(IntentAction.resume) + ) + } else { + builder.addAction( + R.drawable.ic_pause_24dp, + getString(R.string.pause), + aloudServicePendingIntent(IntentAction.pause) + ) + } + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.stop), + aloudServicePendingIntent(IntentAction.stop) + ) + builder.addAction( + R.drawable.ic_time_add_24dp, + getString(R.string.set_timer), + aloudServicePendingIntent(IntentAction.addTimer) + ) + builder.setStyle( + androidx.media.app.NotificationCompat.MediaStyle() + .setShowActionsInCompactView(0, 1, 2) + ) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + val notification = builder.build() + startForeground(AppConst.notificationIdRead, notification) + } + + abstract fun aloudServicePendingIntent(actionStr: String): PendingIntent? + + open fun nextChapter() { + ReadBook.upReadStartTime() + if (!ReadBook.moveToNextChapter(true)) { + stopSelf() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/service/CacheBookService.kt b/app/src/main/java/com/novel/read/service/CacheBookService.kt new file mode 100644 index 0000000..9e349fc --- /dev/null +++ b/app/src/main/java/com/novel/read/service/CacheBookService.kt @@ -0,0 +1,281 @@ +package com.novel.read.service + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.core.app.NotificationCompat +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseService +import com.novel.read.constant.AppConst +import com.novel.read.constant.EventBus +import com.novel.read.constant.IntentAction +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.help.AppConfig +import com.novel.read.help.BookHelp +import com.novel.read.help.IntentHelp +import com.novel.read.service.help.CacheBook +import com.novel.read.utils.ext.postEvent +import com.novel.read.help.coroutine.CompositeCoroutine +import com.novel.read.help.coroutine.Coroutine +import com.novel.read.network.repository.BookRepository +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.isActive +import org.jetbrains.anko.toast +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.Executors + +class CacheBookService : BaseService() { + private val threadCount = AppConfig.threadCount + private var searchPool = + Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + private var tasks = CompositeCoroutine() + private val handler = Handler(Looper.getMainLooper()) + private var runnable: Runnable = Runnable { upDownload() } + private val bookMap = ConcurrentHashMap() + private val downloadMap = ConcurrentHashMap>() + private val downloadCount = ConcurrentHashMap() + private val finalMap = ConcurrentHashMap>() + private val downloadingList = CopyOnWriteArraySet() + private val bookRepository by lazy { BookRepository() } + + @Volatile + private var downloadingCount = 0 + private var notificationContent = App.INSTANCE.getString(R.string.starting_download) + + private val notificationBuilder by lazy { + val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_download) + .setOngoing(true) + .setContentTitle(getString(R.string.offline_cache)) + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + IntentHelp.servicePendingIntent(this, IntentAction.stop) + ) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } + + override fun onCreate() { + super.onCreate() + updateNotification(notificationContent) + handler.postDelayed(runnable, 1000) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.action?.let { action -> + when (action) { + IntentAction.start -> addDownloadData( + intent.getLongExtra("bookId", 0), + intent.getIntExtra("start", 0), + intent.getIntExtra("end", 0) + ) + IntentAction.remove -> removeDownload(intent.getLongExtra("bookId", 0)) + IntentAction.stop -> stopDownload() + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + tasks.clear() + searchPool.close() + handler.removeCallbacks(runnable) + downloadMap.clear() + finalMap.clear() + super.onDestroy() + postEvent(EventBus.UP_DOWNLOAD, downloadMap) + } + + private fun getBook(bookId: Long): Book? { + var book = bookMap[bookId] + if (book == null) { + synchronized(this) { + book = bookMap[bookId] + if (book == null) { + book = App.db.getBookDao().getBook(bookId.toString()) + if (book == null) { + removeDownload(bookId) + } + } + } + } + return book + } + + private fun addDownloadData(bookId: Long, start: Int, end: Int) { + bookId ?: return + if (downloadMap.containsKey(bookId)) { + updateNotification(getString(R.string.already_in_download)) + toast(R.string.already_in_download) + return + } + downloadCount[bookId] = DownloadCount() + execute { + App.db.getChapterDao().getChapterList(bookId, start, end).let { + if (it.isNotEmpty()) { + val chapters = CopyOnWriteArraySet() + chapters.addAll(it) + downloadMap[bookId] = chapters + } else { + CacheBook.addLog("${getBook(bookId)?.bookName} is empty") + } + } + for (i in 0 until threadCount) { + if (downloadingCount < threadCount) { + download() + } + } + } + } + + private fun removeDownload(bookId: Long?) { + downloadMap.remove(bookId) + finalMap.remove(bookId) + } + + private fun download() { + downloadingCount += 1 + val task = Coroutine.async(this, context = searchPool) { + if (!isActive) return@async + val bookChapter: BookChapter? = synchronized(this@CacheBookService) { + downloadMap.forEach { + it.value.forEach { chapter -> + if (!downloadingList.contains(chapter.chapterId)) { + downloadingList.add(chapter.chapterId) + return@synchronized chapter + } + } + } + return@synchronized null + } + if (bookChapter == null) { + postDownloading(false) + } else { + val book = getBook(bookChapter.bookId) + if (book == null) { + postDownloading(true) + return@async + } + if (!BookHelp.hasContent(book, bookChapter)) { + + bookRepository.getContents(this, book, bookChapter) + .timeout(60000L) + .onSuccess { + val content = it.chapter.chapterContent + if (content.isNotBlank()) { + BookHelp.saveContent(book, bookChapter, content) + } + synchronized(this@CacheBookService) { + downloadCount[book.bookId]?.increaseSuccess() + downloadCount[book.bookId]?.increaseFinished() + downloadCount[book.bookId]?.let { + updateNotification( + it, + downloadMap[book.bookId]?.size, + bookChapter.chapterName + ) + } + val chapterMap = + finalMap[book.bookId] + ?: CopyOnWriteArraySet().apply { + finalMap[book.bookId] = this + } + chapterMap.add(bookChapter) + if (chapterMap.size == downloadMap[book.bookId]?.size) { + downloadMap.remove(book.bookId) + finalMap.remove(book.bookId) + downloadCount.remove(book.bookId) + } + } + }.onError { + synchronized(this) { + downloadingList.remove(bookChapter.chapterId) + } + notificationContent = "getContentError${it.localizedMessage}" + upNotification() + }.onFinally { + postDownloading(true) + } + } else { + //无需下载的,设置为增加成功 + downloadCount[book.bookId]?.increaseSuccess() + downloadCount[book.bookId]?.increaseFinished() + postDownloading(true) + } + } + }.onError { + CacheBook.addLog("ERROR:${it.localizedMessage}") + updateNotification("ERROR:${it.localizedMessage}") + } + tasks.add(task) + } + + /** + * 更新通知 + */ + private fun upNotification() { + notificationBuilder.setContentText(notificationContent) + val notification = notificationBuilder.build() + startForeground(AppConst.notificationIdDownload, notification) + } + + private fun postDownloading(hasChapter: Boolean) { + downloadingCount -= 1 + if (hasChapter) { + download() + } else { + if (downloadingCount < 1) { + stopDownload() + } + } + } + + private fun stopDownload() { + tasks.clear() + stopSelf() + } + + private fun upDownload() { + updateNotification(notificationContent) + postEvent(EventBus.UP_DOWNLOAD, downloadMap) + handler.removeCallbacks(runnable) + handler.postDelayed(runnable, 1000) + } + + private fun updateNotification( + downloadCount: DownloadCount, + totalCount: Int?, + content: String + ) { + notificationContent = + "进度:" + downloadCount.downloadFinishedCount + "/" + totalCount + ",成功:" + downloadCount.successCount + "," + content + } + + /** + * 更新通知 + */ + private fun updateNotification(content: String) { + notificationBuilder.setContentText(content) + val notification = notificationBuilder.build() + startForeground(AppConst.notificationIdDownload, notification) + } + + class DownloadCount { + @Volatile + var downloadFinishedCount = 0 // 下载完成的条目数量 + + @Volatile + var successCount = 0 //下载成功的条目数量 + + fun increaseSuccess() { + ++successCount + } + + fun increaseFinished() { + ++downloadFinishedCount + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/service/DownloadMessage.java b/app/src/main/java/com/novel/read/service/DownloadMessage.java deleted file mode 100644 index 6b35680..0000000 --- a/app/src/main/java/com/novel/read/service/DownloadMessage.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.novel.read.service; - -import com.mango.mangolib.event.BaseEvent; - -public class DownloadMessage extends BaseEvent { - - public String message; - - public DownloadMessage(String message){ - this.message = message; - } -} diff --git a/app/src/main/java/com/novel/read/service/DownloadService.java b/app/src/main/java/com/novel/read/service/DownloadService.java deleted file mode 100644 index ba880f4..0000000 --- a/app/src/main/java/com/novel/read/service/DownloadService.java +++ /dev/null @@ -1,444 +0,0 @@ -package com.novel.read.service; - -import android.content.Context; -import android.content.Intent; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.mango.mangolib.event.EventManager; -import com.novel.read.R; -import com.novel.read.base.BaseService; -import com.novel.read.event.DeleteResponseEvent; -import com.novel.read.event.DeleteTaskEvent; -import com.novel.read.event.RxBus; -import com.novel.read.http.AccountManager; -import com.novel.read.model.db.BookChapterBean; -import com.novel.read.model.db.DownloadTaskBean; -import com.novel.read.model.db.dbManage.BookManager; -import com.novel.read.model.db.dbManage.BookRepository; -import com.novel.read.utlis.LocalManageUtil; -import com.novel.read.utlis.NetworkUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; - -public class DownloadService extends BaseService { - private static final String TAG = "DownloadService"; - //加载状态 - private static final int LOAD_ERROR = -1; - private static final int LOAD_NORMAL = 0; - private static final int LOAD_PAUSE = 1; - private static final int LOAD_DELETE = 2; //正在加载时候,用户删除收藏书籍的情况。 - - //下载状态 - public static final int STATUS_CONTINUE = DownloadTaskBean.STATUS_LOADING; - public static final int STATUS_PAUSE = DownloadTaskBean.STATUS_PAUSE; - - //线程池 - private final ExecutorService mSingleExecutor = Executors.newSingleThreadExecutor(); - //加载队列 - private final List mDownloadTaskQueue = Collections.synchronizedList(new ArrayList<>()); - //Handler - private Handler mHandler; - - //包含所有的DownloadTask - private List mDownloadTaskList; - - private OnDownloadListener mDownloadListener; - private boolean isBusy = false; - private boolean isCancel = false; - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(LocalManageUtil.setLocal(base)); - } - - @Override - public void onCreate() { - super.onCreate(); - mHandler = new Handler(getMainLooper()); - //从数据库中获取所有的任务 - mDownloadTaskList = BookRepository - .getInstance() - .getDownloadTaskList(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return new TaskBuilder(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - - //接受创建的DownloadTask - Disposable disposable = RxBus.getInstance() - .toObservable(DownloadTaskBean.class) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - (event) -> { - //判断任务是否为轮询标志 - //判断任务是否存在,并修改任务 - if (TextUtils.isEmpty(event.getBookId()) || !checkAndAlterDownloadTask(event)) { - addToExecutor(event); - } - } - ); - addDisposable(disposable); - - //是否删除数据的问题 - Disposable deleteDisp = RxBus.getInstance() - .toObservable(DeleteTaskEvent.class) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - (event) -> { - //判断是否该数据存在加载列表中 - boolean isDelete = true; - for (DownloadTaskBean bean : mDownloadTaskQueue) { - if (bean.getBookId().equals(event.getCollBook().getId())) { - isDelete = false; - break; - } - } - //如果不存在则删除List中的task - if (isDelete) { - // - Iterator taskIt = mDownloadTaskList.iterator(); - while (taskIt.hasNext()) { - DownloadTaskBean task = taskIt.next(); - if (task.getBookId().equals(event.getCollBook().getId())) { - taskIt.remove(); - } - } - } - //返回状态 - RxBus.getInstance().post(new DeleteResponseEvent(isDelete, event.getCollBook())); - } - ); - addDisposable(deleteDisp); - return super.onStartCommand(intent, flags, startId); - } - - - /** - * 1. 查看是否任务已存在 - * 2. 修改DownloadTask的 taskName 和 list - * - * @return - */ - private boolean checkAndAlterDownloadTask(DownloadTaskBean newTask) { - boolean isExist = false; - for (DownloadTaskBean downloadTask : mDownloadTaskList) { - //如果不相同则不往下执行,往下执行都是存在相同的情况 - if (!downloadTask.getTaskName().equals(newTask.getTaskName())) continue; - - if (downloadTask.getStatus() == DownloadTaskBean.STATUS_FINISH) { - //判断是否newTask是已完成 - if (downloadTask.getLastChapter() == newTask.getLastChapter()) { - isExist = true; - //发送回去已缓存 - postMessage(getString(R.string.cached)); - } - //判断,是否已完成的章节的起始点比新Task大,如果更大则表示新Task中的该章节已被加载,所以需要剪切 - else if (downloadTask.getLastChapter() > (newTask.getLastChapter() - newTask.getBookChapterList().size())) { - if (downloadTask.getLastChapter() > newTask.getLastChapter()) { - isExist = true; - //发送回去已缓存 - postMessage(getString(R.string.cached)); - } else { - //删除掉已经完成的章节 - List remainChapterBeans = newTask.getBookChapterList().subList(downloadTask.getLastChapter(), newTask.getLastChapter()); - String taskName = newTask.getTaskName() - + getString(R.string.download_chapter_scope, - downloadTask.getLastChapter(), newTask.getLastChapter()); - //重置任务 - newTask.setBookChapters(remainChapterBeans); - newTask.setTaskName(taskName); - - //发送添加到任务的提示 - postMessage(getString(R.string.cache_success)); - } - } - } - //表示该任务已经在 下载、等待、暂停、网络错误中 - else { - isExist = true; - //发送回去:已经在加载队列中。 - postMessage(getString(R.string.task_exist)); - } - } - //重置名字 - if (!isExist) { - String taskName = newTask.getTaskName() - + getString(R.string.download_chapter_scope, - 1, newTask.getLastChapter()); - newTask.setTaskName(taskName); - postMessage(getString(R.string.cache_success)); - } - return isExist; - } - - private void addToExecutor(DownloadTaskBean taskEvent) { - - //判断是否为轮询请求 - if (!TextUtils.isEmpty(taskEvent.getBookId())) { - - if (!mDownloadTaskList.contains(taskEvent)) { - //加入总列表中,表示创建,修改CollBean的状态。 - mDownloadTaskList.add(taskEvent); - } - // 添加到下载队列 - mDownloadTaskQueue.add(taskEvent); - } - - // 从队列顺序取出第一条下载 - if (mDownloadTaskQueue.size() > 0 && !isBusy) { - isBusy = true; - executeTask(mDownloadTaskQueue.get(0)); - } - } - - private void executeTask(DownloadTaskBean taskEvent) { - Runnable runnable = () -> { - - taskEvent.setStatus(DownloadTaskBean.STATUS_LOADING); - - int result = LOAD_NORMAL; - List bookChapterBeans = taskEvent.getBookChapters(); - - //调用for循环,下载数据 //todo -// for (int i=taskEvent.getCurrentChapter(); i { - //TODO:这里文件的名字用的是BookChapter的title,而不是chapter的title。 - //原因是Chapter的title可能重复,但是BookChapter的title不会重复 - //BookChapter的title = 卷名 + 章节名 chapter 的 title 就是章节名。。 - BookRepository.getInstance().saveChapterInfo(folderName, bean.getTitle(), chapterInfo.getBody()); - }, - e -> { - //当前进度加载错误(这里需要判断是什么问题,根据相应的问题做出相应的回答) - Log.e(TAG, "loadChapter: " + e); - //设置加载结果 - result[0] = LOAD_ERROR; - } - ); - addDisposable(disposable); - return result[0]; - } - - private void postDownloadChange(DownloadTaskBean task, int status, String msg) { - if (mDownloadListener != null && msg != null) { - int position = mDownloadTaskList.indexOf(task); - //通过handler,切换回主线程 - mHandler.post(() -> mDownloadListener.onDownloadChange( - position, status, msg) - ); - } - } - - private void postMessage(String msg) { -// RxBus.getInstance().post(new DownloadMessage(msg)); - EventManager.Companion.getInstance().postEvent(new DownloadMessage(msg)); - } - - private void post(DownloadTaskBean task) { - RxBus.getInstance().post(task); - } - - - @Override - public boolean onUnbind(Intent intent) { - - mDownloadListener = null; - return super.onUnbind(intent); - } - - class TaskBuilder extends Binder implements IDownloadManager { - @Override - public List getDownloadTaskList() { - return Collections.unmodifiableList(mDownloadTaskList); - } - - @Override - public void setOnDownloadListener(OnDownloadListener listener) { - mDownloadListener = listener; - } - - @Override - public void setDownloadStatus(String taskName, int status) { - //修改某个Task的状态 - switch (status) { - //加入缓存队列 - case DownloadTaskBean.STATUS_WAIT: - for (int i = 0; i < mDownloadTaskList.size(); ++i) { - DownloadTaskBean bean = mDownloadTaskList.get(i); - if (taskName.equals(bean.getTaskName())) { - bean.setStatus(DownloadTaskBean.STATUS_WAIT); - mDownloadListener.onDownloadResponse(i, DownloadTaskBean.STATUS_WAIT); - addToExecutor(bean); - break; - } - } - break; - //从缓存队列中删除 - case DownloadTaskBean.STATUS_PAUSE: - Iterator it = mDownloadTaskQueue.iterator(); - while (it.hasNext()) { - DownloadTaskBean bean = it.next(); - if (bean.getTaskName().equals(taskName)) { - if (bean.getStatus() == DownloadTaskBean.STATUS_LOADING - && bean.getTaskName().equals(taskName)) { - isCancel = true; - break; - } else { - bean.setStatus(DownloadTaskBean.STATUS_PAUSE); - mDownloadTaskQueue.remove(bean); - int position = mDownloadTaskList.indexOf(bean); - mDownloadListener.onDownloadResponse(position, DownloadTaskBean.STATUS_PAUSE); - break; - } - } - } - break; - } - } - - @Override - public void setAllDownloadStatus(int status) { - //修改所有Task的状态 - } - - //首先判断是否在加载队列中。 - //如果在加载队列中首先判断是否正在下载, - //然后判断是否在完成队列中。 - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - - public interface IDownloadManager { - List getDownloadTaskList(); - - void setOnDownloadListener(OnDownloadListener listener); - - void setDownloadStatus(String taskName, int status); - - void setAllDownloadStatus(int status); - } - - public interface OnDownloadListener { - /** - * @param pos : Task在item中的位置 - * @param status : Task的状态 - * @param msg: 传送的Msg - */ - void onDownloadChange(int pos, int status, String msg); - - /** - * 回复 - */ - void onDownloadResponse(int pos, int status); - } -} diff --git a/app/src/main/java/com/novel/read/service/HttpReadAloudService.kt b/app/src/main/java/com/novel/read/service/HttpReadAloudService.kt new file mode 100644 index 0000000..0024595 --- /dev/null +++ b/app/src/main/java/com/novel/read/service/HttpReadAloudService.kt @@ -0,0 +1,203 @@ +package com.novel.read.service + +import android.app.PendingIntent +import android.content.Intent +import android.media.MediaPlayer +import com.novel.read.constant.EventBus +import com.novel.read.help.IntentHelp +import com.novel.read.service.help.ReadBook +import com.novel.read.utils.FileUtils +import com.novel.read.utils.ext.* +import com.novel.read.help.coroutine.Coroutine +import kotlinx.coroutines.isActive +import java.io.File +import java.io.FileDescriptor +import java.io.FileInputStream + +class HttpReadAloudService : BaseReadAloudService(), + MediaPlayer.OnPreparedListener, + MediaPlayer.OnErrorListener, + MediaPlayer.OnCompletionListener { + + private val mediaPlayer = MediaPlayer() + private lateinit var ttsFolder: String + private var task: Coroutine<*>? = null + private var playingIndex = -1 + + override fun onCreate() { + super.onCreate() + ttsFolder = externalCacheDir!!.absolutePath + File.separator + "httpTTS" + mediaPlayer.setOnErrorListener(this) + mediaPlayer.setOnPreparedListener(this) + mediaPlayer.setOnCompletionListener(this) + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + stopSelf() + } + + override fun onDestroy() { + super.onDestroy() + task?.cancel() + mediaPlayer.release() + } + + override fun newReadAloud(dataKey: String?, play: Boolean) { + mediaPlayer.reset() + playingIndex = -1 + super.newReadAloud(dataKey, play) + } + + override fun play() { + if (contentList.isEmpty()) return + if (nowSpeak == 0) { + downloadAudio() + } else { + val file = getSpeakFile(nowSpeak) + if (file.exists()) { + playAudio(FileInputStream(file).fd) + } + } + } + + private fun downloadAudio() { + task?.cancel() + task = execute { + FileUtils.deleteFile(ttsFolder) + for (index in 0 until contentList.size) { + if (isActive) { +// ReadAloud.httpTTS?.let { +// AnalyzeUrl( +// it.url, +// speakText = contentList[index], +// speakSpeed = AppConfig.ttsSpeechRate +// ).getResponseBytes()?.let { bytes -> +// if (isActive) { +// val file = getSpeakFile(index) +// file.writeBytes(bytes) +// if (index == nowSpeak) { +// @Suppress("BlockingMethodInNonBlockingContext") +// val fis = FileInputStream(file) +// playAudio(fis.fd) +// } +// } +// } +// } + } else { + break + } + } + } + } + + @Synchronized + private fun playAudio(fd: FileDescriptor) { + if (playingIndex != nowSpeak && requestFocus()) { + try { + mediaPlayer.reset() + mediaPlayer.setDataSource(fd) + mediaPlayer.prepareAsync() + playingIndex = nowSpeak + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun getSpeakFile(index: Int = nowSpeak): File { + return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3") + } + + override fun pauseReadAloud(pause: Boolean) { + super.pauseReadAloud(pause) + mediaPlayer.pause() + } + + override fun resumeReadAloud() { + super.resumeReadAloud() + if (playingIndex == -1) { + play() + } else { + mediaPlayer.start() + } + } + + /** + * 更新朗读速度 + */ + override fun upSpeechRate(reset: Boolean) { + task?.cancel() + mediaPlayer.stop() + playingIndex = -1 + downloadAudio() + } + + /** + * 上一段 + */ + override fun prevP() { + if (nowSpeak > 0) { + mediaPlayer.stop() + nowSpeak-- + readAloudNumber -= contentList[nowSpeak].length.minus(1) + play() + } + } + + /** + * 下一段 + */ + override fun nextP() { + if (nowSpeak < contentList.size - 1) { + mediaPlayer.stop() + readAloudNumber += contentList[nowSpeak].length.plus(1) + nowSpeak++ + play() + } + } + + override fun onPrepared(mp: MediaPlayer?) { + super.play() + if (pause) return + mp?.start() + textChapter?.let { + if (readAloudNumber + 1 > it.getReadLength(pageIndex + 1)) { + pageIndex++ + ReadBook.moveToNextPage() + } + } + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) + } + + override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { + if (what == -38 && extra == 0) { + return true + } + handler.postDelayed({ + readAloudNumber += contentList[nowSpeak].length + 1 + if (nowSpeak < contentList.lastIndex) { + nowSpeak++ + play() + } else { + nextChapter() + } + }, 50) + return true + } + + override fun onCompletion(mp: MediaPlayer?) { + readAloudNumber += contentList[nowSpeak].length + 1 + if (nowSpeak < contentList.lastIndex) { + nowSpeak++ + play() + } else { + nextChapter() + } + } + + override fun aloudServicePendingIntent(actionStr: String): PendingIntent? { + return IntentHelp.servicePendingIntent(this, actionStr) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/service/TTSReadAloudService.kt b/app/src/main/java/com/novel/read/service/TTSReadAloudService.kt new file mode 100644 index 0000000..1094dec --- /dev/null +++ b/app/src/main/java/com/novel/read/service/TTSReadAloudService.kt @@ -0,0 +1,193 @@ +package com.novel.read.service + +import android.app.PendingIntent +import android.content.Intent +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener +import com.novel.read.R +import com.novel.read.constant.AppConst +import com.novel.read.constant.EventBus +import com.novel.read.help.AppConfig +import com.novel.read.help.IntentHelp +import com.novel.read.help.MediaHelp +import com.novel.read.service.help.ReadBook +import com.novel.read.utils.ext.* +import kotlinx.coroutines.launch +import org.jetbrains.anko.toast +import java.util.* + +class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener { + + companion object { + private var textToSpeech: TextToSpeech? = null + private var ttsInitFinish = false + + fun clearTTS() { + textToSpeech?.let { + it.stop() + it.shutdown() + } + textToSpeech = null + ttsInitFinish = false + } + } + + private val ttsUtteranceListener = TTSUtteranceListener() + + override fun onCreate() { + super.onCreate() + initTts() + upSpeechRate() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + clearTTS() + stopSelf() + } + + private fun initTts() { + ttsInitFinish = false + textToSpeech = TextToSpeech(this, this) + } + + override fun onDestroy() { + super.onDestroy() + clearTTS() + } + + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + textToSpeech?.let { + it.setOnUtteranceProgressListener(ttsUtteranceListener) + it.language = Locale.CHINA + ttsInitFinish = true + play() + } + } else { + launch { + toast(R.string.tts_init_failed) + } + } + } + + @Synchronized + override fun play() { + if (contentList.isNotEmpty() && ttsInitFinish && requestFocus()) { + super.play() + execute { + MediaHelp.playSilentSound(this@TTSReadAloudService) + textToSpeech?.let { + it.speak("", TextToSpeech.QUEUE_FLUSH, null, null) + for (i in nowSpeak until contentList.size) { + it.speak( + contentList[i], + TextToSpeech.QUEUE_ADD, + null, + AppConst.APP_TAG + i + ) + } + } + } + } + } + + /** + * 更新朗读速度 + */ + override fun upSpeechRate(reset: Boolean) { + if (this.getPrefBoolean("ttsFollowSys", true)) { + if (reset) { + clearTTS() + initTts() + } + } else { + textToSpeech?.setSpeechRate((AppConfig.ttsSpeechRate + 5) / 10f) + } + } + + /** + * 上一段 + */ + override fun prevP() { + if (nowSpeak > 0) { + textToSpeech?.stop() + nowSpeak-- + readAloudNumber -= contentList[nowSpeak].length.minus(1) + play() + } + } + + /** + * 下一段 + */ + override fun nextP() { + if (nowSpeak < contentList.size - 1) { + textToSpeech?.stop() + readAloudNumber += contentList[nowSpeak].length.plus(1) + nowSpeak++ + play() + } + } + + /** + * 暂停朗读 + */ + override fun pauseReadAloud(pause: Boolean) { + super.pauseReadAloud(pause) + textToSpeech?.stop() + } + + /** + * 恢复朗读 + */ + override fun resumeReadAloud() { + super.resumeReadAloud() + play() + } + + /** + * 朗读监听 + */ + private inner class TTSUtteranceListener : UtteranceProgressListener() { + + override fun onStart(s: String) { + textChapter?.let { + if (readAloudNumber + 1 > it.getReadLength(pageIndex + 1)) { + pageIndex++ + ReadBook.moveToNextPage() + } + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) + } + } + + override fun onDone(s: String) { + readAloudNumber += contentList[nowSpeak].length + 1 + nowSpeak++ + if (nowSpeak >= contentList.size) { + nextChapter() + } + } + + override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) { + super.onRangeStart(utteranceId, start, end, frame) + textChapter?.let { + if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) { + pageIndex++ + ReadBook.moveToNextPage() + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + start) + } + } + } + + override fun onError(s: String) { + //nothing + } + + } + + override fun aloudServicePendingIntent(actionStr: String): PendingIntent? { + return IntentHelp.servicePendingIntent(this, actionStr) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/service/help/CacheBook.kt b/app/src/main/java/com/novel/read/service/help/CacheBook.kt new file mode 100644 index 0000000..0f1fb79 --- /dev/null +++ b/app/src/main/java/com/novel/read/service/help/CacheBook.kt @@ -0,0 +1,110 @@ +package com.novel.read.service.help + +import android.content.Context +import android.content.Intent +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.IntentAction +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.help.BookHelp +import com.novel.read.network.repository.BookRepository +import com.novel.read.service.CacheBookService +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet + +object CacheBook { + val logs = arrayListOf() + private val downloadMap = ConcurrentHashMap>() + private val bookRepository by lazy { BookRepository() } + fun addLog(log: String?) { + log ?: return + synchronized(this) { + if (logs.size > 1000) { + logs.removeAt(0) + } + logs.add(log) + } + } + + fun start(context: Context, bookId: Long, start: Int, end: Int) { + Intent(context, CacheBookService::class.java).let { + it.action = IntentAction.start + it.putExtra("bookId", bookId) + it.putExtra("start", start) + it.putExtra("end", end) + context.startService(it) + } + } + + fun remove(context: Context, bookUrl: String) { + Intent(context, CacheBookService::class.java).let { + it.action = IntentAction.remove + it.putExtra("bookUrl", bookUrl) + context.startService(it) + } + } + + fun stop(context: Context) { + Intent(context, CacheBookService::class.java).let { + it.action = IntentAction.stop + context.startService(it) + } + } + + fun downloadCount(): Int { + var count = 0 + downloadMap.forEach { + count += it.value.size + } + return count + } + + fun download( + scope: CoroutineScope, + book: Book, + chapter: BookChapter, + resetPageOffset: Boolean = false + ) { + if (downloadMap[book.bookId]?.contains(chapter.chapterIndex) == true) { + return + } + if (downloadMap[book.bookId] == null) { + downloadMap[book.bookId] = CopyOnWriteArraySet() + } + downloadMap[book.bookId]?.add(chapter.chapterIndex) + + bookRepository.getContents(scope,book,chapter).onSuccess { bookContent-> + val content = bookContent.chapter.chapterContent + + if (content.isNotBlank()) { + BookHelp.saveContent(book, chapter, content) + } + if (ReadBook.book?.bookId == book.bookId) { + ReadBook.contentLoadFinish( + book, + chapter, + content.ifBlank { App.INSTANCE.getString(R.string.content_empty) }, + resetPageOffset = resetPageOffset + ) + } + }.onError { + if (ReadBook.book?.bookId == book.bookId) { + ReadBook.contentLoadFinish( + book, + chapter, + it.message!!, + resetPageOffset = resetPageOffset + ) + } + }.onFinally { + downloadMap[book.bookId]?.remove(chapter.chapterIndex) + if (downloadMap[book.bookId].isNullOrEmpty()) { + downloadMap.remove(book.bookId) + } + ReadBook.removeLoading(chapter.chapterIndex) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/service/help/ReadAloud.kt b/app/src/main/java/com/novel/read/service/help/ReadAloud.kt new file mode 100644 index 0000000..d5c606d --- /dev/null +++ b/app/src/main/java/com/novel/read/service/help/ReadAloud.kt @@ -0,0 +1,108 @@ +package com.novel.read.service.help + +import android.content.Context +import android.content.Intent +import com.novel.read.App +import com.novel.read.constant.IntentAction +import com.novel.read.constant.PreferKey +import com.novel.read.data.db.entity.HttpTTS +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.HttpReadAloudService +import com.novel.read.service.TTSReadAloudService +import com.novel.read.utils.ext.* + +object ReadAloud { + private var aloudClass: Class<*> = getReadAloudClass() + var httpTTS: HttpTTS? = null + + private fun getReadAloudClass(): Class<*> { + val spId = App.INSTANCE.getPrefLong(PreferKey.speakEngine) +// httpTTS = App.db.httpTTSDao().get(spId) + return if (httpTTS != null) { + HttpReadAloudService::class.java + } else { + TTSReadAloudService::class.java + } + } + + fun upReadAloudClass() { + stop(App.INSTANCE) + aloudClass = getReadAloudClass() + } + + fun play( + context: Context, + title: String, + subtitle: String, + pageIndex: Int, + dataKey: String, + play: Boolean = true + ) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.play + intent.putExtra("title", title) + intent.putExtra("subtitle", subtitle) + intent.putExtra("pageIndex", pageIndex) + intent.putExtra("dataKey", dataKey) + intent.putExtra("play", play) + context.startService(intent) + } + + fun pause(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.pause + context.startService(intent) + } + } + + fun resume(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.resume + context.startService(intent) + } + } + + fun stop(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.stop + context.startService(intent) + } + } + + fun prevParagraph(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.prevParagraph + context.startService(intent) + } + } + + fun nextParagraph(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.nextParagraph + context.startService(intent) + } + } + + fun upTtsSpeechRate(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.upTtsSpeechRate + context.startService(intent) + } + } + + fun setTimer(context: Context, minute: Int) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.setTimer + intent.putExtra("minute", minute) + context.startService(intent) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/service/help/ReadBook.kt b/app/src/main/java/com/novel/read/service/help/ReadBook.kt new file mode 100644 index 0000000..ad12f61 --- /dev/null +++ b/app/src/main/java/com/novel/read/service/help/ReadBook.kt @@ -0,0 +1,444 @@ +package com.novel.read.service.help + +import androidx.lifecycle.MutableLiveData +import com.hankcs.hanlp.HanLP +import com.novel.read.App +import com.novel.read.constant.AppConst +import com.novel.read.constant.BookType +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.data.db.entity.ReadRecord +import com.novel.read.data.read.TextChapter +import com.novel.read.data.read.TextPage +import com.novel.read.help.AppConfig +import com.novel.read.help.BookHelp +import com.novel.read.help.IntentDataHelp +import com.novel.read.help.ReadBookConfig +import com.novel.read.service.BaseReadAloudService +import com.novel.read.help.coroutine.Coroutine +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.ImageProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.anko.getStackTraceString +import org.jetbrains.anko.toast + + +object ReadBook { + var titleDate = MutableLiveData() + var book: Book? = null + var inBookshelf = false + var chapterSize = 0 + var durChapterIndex = 0 + var durPageIndex = 0 + var isLocalBook = true + var callBack: CallBack? = null + var prevTextChapter: TextChapter? = null + var curTextChapter: TextChapter? = null + var nextTextChapter: TextChapter? = null + var msg: String? = null + private val loadingChapters = arrayListOf() + private val readRecord = ReadRecord() + var readStartTime: Long = System.currentTimeMillis() + + fun resetData(book: Book) { + this.book = book + readRecord.bookName = book.bookName + readRecord.readTime = App.db.getReadRecordDao().getReadTime(book.bookName) ?: 0 + durChapterIndex = book.durChapterIndex + durPageIndex = book.durChapterPos + isLocalBook = book.origin == BookType.local + chapterSize = 0 + prevTextChapter = null + curTextChapter = null + nextTextChapter = null + titleDate.postValue(book.bookName) + ImageProvider.clearAllCache() + synchronized(this) { + loadingChapters.clear() + } + } + + + fun upReadStartTime() { + Coroutine.async { + readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime + readStartTime = System.currentTimeMillis() + App.db.getReadRecordDao().insert(readRecord) + } + } + + fun upMsg(msg: String?) { + if (this.msg != msg) { + this.msg = msg + callBack?.upContent() + } + } + + fun moveToNextPage() { + durPageIndex++ + callBack?.upContent() + saveRead() + } + + fun moveToNextChapter(upContent: Boolean): Boolean { + if (durChapterIndex < chapterSize - 1) { + durPageIndex = 0 + durChapterIndex++ + prevTextChapter = curTextChapter + curTextChapter = nextTextChapter + nextTextChapter = null + book?.let { + if (curTextChapter == null) { + loadContent(durChapterIndex, upContent, false) + } else if (upContent) { + callBack?.upContent() + } + loadContent(durChapterIndex.plus(1), upContent, false) + GlobalScope.launch(Dispatchers.IO) { + for (i in 2..10) { + delay(100) + download(durChapterIndex + i) + } + } + } + saveRead() + callBack?.upView() + curPageChanged() + return true + } else { + return false + } + } + + fun moveToPrevChapter(upContent: Boolean, toLast: Boolean = true): Boolean { + if (durChapterIndex > 0) { + durPageIndex = if (toLast) prevTextChapter?.lastIndex ?: 0 else 0 + durChapterIndex-- + nextTextChapter = curTextChapter + curTextChapter = prevTextChapter + prevTextChapter = null + book?.let { + if (curTextChapter == null) { + loadContent(durChapterIndex, upContent, false) + } else if (upContent) { + callBack?.upContent() + } + loadContent(durChapterIndex.minus(1), upContent, false) + GlobalScope.launch(Dispatchers.IO) { + for (i in -5..-2) { + delay(100) + download(durChapterIndex + i) + } + } + } + saveRead() + callBack?.upView() + curPageChanged() + return true + } else { + return false + } + } + + fun skipToPage(page: Int) { + durPageIndex = page + callBack?.upContent() + curPageChanged() + saveRead() + } + + fun setPageIndex(pageIndex: Int) { + durPageIndex = pageIndex + saveRead() + curPageChanged() + } + + private fun curPageChanged() { + callBack?.pageChanged() + if (BaseReadAloudService.isRun) { + readAloud(!BaseReadAloudService.pause) + } + upReadStartTime() + } + + /** + * 朗读 + */ + fun readAloud(play: Boolean = true) { + val book = book + val textChapter = curTextChapter + if (book != null && textChapter != null) { + val key = IntentDataHelp.putData(textChapter) + ReadAloud.play( + App.INSTANCE, + book.bookName, + textChapter.title, + durPageIndex, + key, + play + ) + } + } + + fun durChapterPos(): Int { + curTextChapter?.let { + if (durPageIndex < it.pageSize) { + return durPageIndex + } + return it.pageSize - 1 + } + return durPageIndex + } + + /** + * chapterOnDur: 0为当前页,1为下一页,-1为上一页 + */ + fun textChapter(chapterOnDur: Int = 0): TextChapter? { + return when (chapterOnDur) { + 0 -> curTextChapter + 1 -> nextTextChapter + -1 -> prevTextChapter + else -> null + } + } + + /** + * 加载章节内容 + */ + fun loadContent(resetPageOffset: Boolean) { + loadContent(durChapterIndex, resetPageOffset = resetPageOffset) + loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset) + loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset) + } + + fun loadContent(index: Int, upContent: Boolean = true, resetPageOffset: Boolean) { + book?.let { book -> + if (addLoading(index)) { + Coroutine.async { + App.db.getChapterDao().getChapter(book.bookId, index)?.let { chapter -> + BookHelp.getContent(book, chapter)?.let { + contentLoadFinish(book, chapter, it, upContent, resetPageOffset) + removeLoading(chapter.chapterIndex) + } ?: download(chapter, resetPageOffset = resetPageOffset) + } ?: removeLoading(index) + }.onError { + removeLoading(index) + } + } + } + } + + private fun download(index: Int) { + book?.let { book -> + if (book.isLocalBook()) return + if (addLoading(index)) { + Coroutine.async { + App.db.getChapterDao().getChapter(book.bookId, index)?.let { chapter -> + if (BookHelp.hasContent(book, chapter)) { + removeLoading(chapter.chapterIndex) + } else { + download(chapter, false) + } + } ?: removeLoading(index) + }.onError { + removeLoading(index) + } + } + } + } + + private fun download(chapter: BookChapter, resetPageOffset: Boolean) { + val book = book + if (book != null) { + CacheBook.download(Coroutine.DEFAULT,book, chapter) + } else if (book != null) { + contentLoadFinish( + book, + chapter, + "没有书源", + resetPageOffset = resetPageOffset + ) + removeLoading(chapter.chapterIndex) + } else { + removeLoading(chapter.chapterIndex) + } + } + + private fun addLoading(index: Int): Boolean { + synchronized(this) { + if (loadingChapters.contains(index)) return false + loadingChapters.add(index) + return true + } + } + + fun removeLoading(index: Int) { + synchronized(this) { + loadingChapters.remove(index) + } + } + + fun searchResultPositions( + pages: List, + indexWithinChapter: Int, + query: String + ): Array { + // calculate search result's pageIndex + var content = "" + pages.map { + content += it.text + } + var count = 1 + var index = content.indexOf(query) + while (count != indexWithinChapter) { + index = content.indexOf(query, index + 1) + count += 1 + } + val contentPosition = index + var pageIndex = 0 + var length = pages[pageIndex].text.length + while (length < contentPosition) { + pageIndex += 1 + if (pageIndex > pages.size) { + pageIndex = pages.size + break + } + length += pages[pageIndex].text.length + } + + // calculate search result's lineIndex + val currentPage = pages[pageIndex] + var lineIndex = 0 + length = length - currentPage.text.length + currentPage.textLines[lineIndex].text.length + while (length < contentPosition) { + lineIndex += 1 + if (lineIndex > currentPage.textLines.size) { + lineIndex = currentPage.textLines.size + break + } + length += currentPage.textLines[lineIndex].text.length + } + + // charIndex + val currentLine = currentPage.textLines[lineIndex] + length -= currentLine.text.length + val charIndex = contentPosition - length + var addLine = 0 + var charIndex2 = 0 + // change line + if ((charIndex + query.length) > currentLine.text.length) { + addLine = 1 + charIndex2 = charIndex + query.length - currentLine.text.length - 1 + } + // changePage + if ((lineIndex + addLine + 1) > currentPage.textLines.size) { + addLine = -1 + charIndex2 = charIndex + query.length - currentLine.text.length - 1 + } + return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2) + } + + /** + * 内容加载完成 + */ + fun contentLoadFinish( + book: Book, + chapter: BookChapter, + content: String, + upContent: Boolean = true, + resetPageOffset: Boolean + ) { + Coroutine.async { + if (chapter.chapterIndex in durChapterIndex - 1..durChapterIndex + 1) { + chapter.chapterName = when (AppConfig.chineseConverterType) { + 1 -> HanLP.convertToSimplifiedChinese(chapter.chapterName) + 2 -> HanLP.convertToTraditionalChinese(chapter.chapterName) + else -> chapter.chapterName + } + val contents = BookHelp.disposeContent( + book, + chapter.chapterName, + content + ) + when (chapter.chapterIndex) { + durChapterIndex -> { + curTextChapter = + ChapterProvider.getTextChapter( + book, + chapter, + contents, + chapterSize, + "" + ) + if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) + callBack?.upView() + curPageChanged() + callBack?.contentLoadFinish() + ImageProvider.clearOut(durChapterIndex) + } + durChapterIndex - 1 -> { + prevTextChapter = + ChapterProvider.getTextChapter( + book, + chapter, + contents, + chapterSize, + "" + ) + if (upContent) callBack?.upContent(-1, resetPageOffset) + } + durChapterIndex + 1 -> { + nextTextChapter = + ChapterProvider.getTextChapter( + book, + chapter, + contents, + chapterSize, + "" + ) + if (upContent) callBack?.upContent(1, resetPageOffset) + } + } + } + }.onError { + it.printStackTrace() + App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}") + } + } + + fun pageAnim(): Int { +// book?.let { +// return if (it.getPageAnim() < 0) +// ReadBookConfig.pageAnim +// else +// it.getPageAnim() +// } + return ReadBookConfig.pageAnim + } + + fun saveRead() { + Coroutine.async { + book?.let { book -> + book.durChapterTime = System.currentTimeMillis() + book.durChapterIndex = durChapterIndex + book.durChapterPos = durPageIndex + App.db.getChapterDao().getChapter(book.bookId, durChapterIndex)?.let { + book.durChapterTitle = it.chapterName + } + App.db.getBookDao().update(book) + } + } + } + + interface CallBack { + fun loadChapterList(book: Book) + fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) + fun upView() + fun pageChanged() + fun contentLoadFinish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/MainActivity.kt b/app/src/main/java/com/novel/read/ui/MainActivity.kt new file mode 100644 index 0000000..c438612 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/MainActivity.kt @@ -0,0 +1,195 @@ +package com.novel.read.ui + +import android.os.Bundle +import android.view.KeyEvent +import android.view.MenuItem +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.observe +import androidx.viewpager.widget.ViewPager +import com.allenliu.versionchecklib.v2.AllenVersionChecker +import com.allenliu.versionchecklib.v2.builder.UIData +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.EventBus +import com.novel.read.data.model.AppUpdateResp +import com.novel.read.help.AppConfig +import com.novel.read.lib.ATH +import com.novel.read.service.BaseReadAloudService +import com.novel.read.ui.main.bookshelf.BookshelfFragment +import com.novel.read.ui.main.mail.MailFragment +import com.novel.read.ui.main.my.MyFragment +import com.novel.read.ui.widget.dialog.AppraiseDialog +import com.novel.read.user.VipHelper +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.activity_main.* +import org.jetbrains.anko.toast + +class MainActivity : VMBaseActivity(R.layout.activity_main), + BottomNavigationView.OnNavigationItemSelectedListener, + BottomNavigationView.OnNavigationItemReselectedListener, + ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() { + + override val viewModel: MainViewModel + get() = getViewModel(MainViewModel::class.java) + + private var bookshelfReselected: Long = 0 + private var exitTime: Long = 0 + private var pagePosition = 0 + private val fragmentMap = hashMapOf() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(view_pager_main) + ATH.applyBottomNavigationColor(bottom_navigation_view) + + view_pager_main.offscreenPageLimit = 3 + view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager) + view_pager_main.addOnPageChangeListener(this) + bottom_navigation_view.elevation = + if (AppConfig.elevation < 0) elevation else AppConfig.elevation.toFloat() + bottom_navigation_view.setOnNavigationItemSelectedListener(this) + bottom_navigation_view.setOnNavigationItemReselectedListener(this) + initData() + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + viewModel.appUpdate() + viewModel.checkCount() + //自动更新书籍 + view_pager_main.postDelayed({ + viewModel.upAllBookToc() + }, 1000) + } + + + private fun initData() { + VipHelper.notifyUser() + viewModel.appResp.observe(this) { + updateApk(it) + } + viewModel.showEvaluate.observe(this) { + val dialog = AppraiseDialog(this) + dialog.appraiseDialog { + goShop() + dialog.dismiss() + } + dialog.show() + } + } + + override fun onPageSelected(position: Int) { + view_pager_main.hideSoftInput() + pagePosition = position + when (position) { + 0, 1, 2 -> bottom_navigation_view.menu.getItem(position).isChecked = true + } + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + event?.let { + when (keyCode) { + KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled) { + if (pagePosition != 0) { + view_pager_main.currentItem = 0 + return true + } + if (System.currentTimeMillis() - exitTime > 2000) { + toast(R.string.double_click_exit) + exitTime = System.currentTimeMillis() + } else { + if (BaseReadAloudService.pause) { + finish() + } else { + moveTaskToBack(true) + } + } + return true + } + } + } + return super.onKeyUp(keyCode, event) + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_mail -> view_pager_main.setCurrentItem(0, false) + R.id.menu_bookshelf -> view_pager_main.setCurrentItem(1, false) + R.id.menu_my_config -> view_pager_main.setCurrentItem(2, false) + } + return false + } + + override fun onNavigationItemReselected(item: MenuItem) { + when (item.itemId) { + R.id.menu_bookshelf -> { + if (System.currentTimeMillis() - bookshelfReselected > 300) { + bookshelfReselected = System.currentTimeMillis() + } else { + (fragmentMap[0] as? BookshelfFragment)?.gotoTop() + } + } + } + } + + + private inner class TabFragmentPageAdapter(fm: FragmentManager) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + private fun getId(position: Int): Int { + return position + } + + override fun getItemPosition(`object`: Any): Int { + return POSITION_NONE + } + + override fun getItem(position: Int): Fragment { + return when (getId(position)) { + 0 -> fragmentMap[0] ?: MailFragment() + 1 -> fragmentMap[1] ?: BookshelfFragment() + else -> fragmentMap[2] ?: MyFragment() + } + } + + override fun getCount(): Int { + return 3 + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val fragment = super.instantiateItem(container, position) as Fragment + fragmentMap[getId(position)] = fragment + return fragment + } + + } + + override fun observeLiveBus() { + observeEvent(EventBus.RECREATE) { + recreate() + } + } + + + private fun updateApk(resp: AppUpdateResp?) { + val versionBean = resp?.appEdition + if (versionBean != null) { + val builder = AllenVersionChecker + .getInstance() + .downloadOnly( + UIData.create() + .setTitle(getString(R.string.new_version, versionBean.editionCode)) + .setContent(versionBean.upgradeContent) + .setDownloadUrl(versionBean.fileUrl) + ) + if (versionBean.forceUpdate == 1) { //强制更新 + builder.setForceUpdateListener(::finish) + } + builder.executeMission(this) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/MainViewModel.kt b/app/src/main/java/com/novel/read/ui/MainViewModel.kt new file mode 100644 index 0000000..94057b9 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/MainViewModel.kt @@ -0,0 +1,143 @@ +package com.novel.read.ui + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.BookType +import com.novel.read.constant.EventBus +import com.novel.read.constant.PreferKey +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.data.model.AppUpdateResp +import com.novel.read.data.model.ChapterResp +import com.novel.read.network.repository.HomeRepository +import com.novel.read.service.help.ReadBook +import com.novel.read.utils.StringUtils +import com.novel.read.utils.ext.* +import com.novel.read.help.coroutine.Coroutine +import com.novel.read.network.repository.BookRepository +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet + +class MainViewModel(application: Application) : BaseViewModel(application) { + + private val homeRepository by lazy { HomeRepository() } + private val bookRepository by lazy { BookRepository() } + var appResp = MutableLiveData() + val showEvaluate = MutableLiveData() + + fun appUpdate() { + launch({ + appResp.value = homeRepository.appUpdate() + }, showErrorToast = false) + } + + fun upAllBookToc() { + execute { + upToc(App.db.getBookDao().getAllBooks()) + } + } + + val updateList = CopyOnWriteArraySet() + private val bookMap = ConcurrentHashMap() + + fun upToc(books: List) { + execute { + books.filter { + it.origin != BookType.local && it.canUpdate() + }.forEach { + bookMap[it.bookId] = it + } + updateToc() + } + } + + @Synchronized + private fun updateToc() { + bookMap.forEach { bookEntry -> + if (!updateList.contains(bookEntry.key)) { + val book = bookEntry.value + synchronized(this) { + updateList.add(book.bookId) + postEvent(EventBus.UPDATE_BOOK, book.bookId) + } + + launch( + block = { + val bookChapters = bookRepository.getDirectory(book.bookId) + dealData(bookChapters, bookEntry) + }, + error = { + it.printStackTrace() + synchronized(this) { + bookMap.remove(bookEntry.key) + updateList.remove(book.bookId) + postEvent(EventBus.UPDATE_BOOK, book.bookId) + upNext() + } + }, + showErrorToast = false, + ) + return + } + } + } + + private fun dealData(chapterResp: ChapterResp, bookE: Map.Entry) { + val book = bookE.value + Coroutine.async { + val cList = arrayListOf() + for (chapter in chapterResp.chapterList!!) { + val bookChapter = BookChapter( + bookId = chapter.bookId, + chapterId = chapter.chapterId, + chapterIndex = chapter.chapterIndex - 1, + chapterName = chapter.chapterName, + createTimeValue = chapter.createTime, + updateDate = "", + updateTimeValue = 0L, + chapterUrl = chapter.chapterUrl + ) + cList.add(bookChapter) + } + if (cList.isNotEmpty()) { + App.db.getChapterDao().insert(cList.toTypedArray()) + book.totalChapterNum = cList.size + book.lastUpdateChapterDate = cList[cList.size - 1].updateDate + App.db.getBookDao().update(book) + } else { + ReadBook.upMsg(context.getString(R.string.error_load_toc)) + } + }.onFinally { + synchronized(this) { + bookMap.remove(bookE.key) + updateList.remove(book.bookId) + postEvent(EventBus.UPDATE_BOOK, book.bookId) + upNext() + } + } + } + + private fun upNext() { + if (bookMap.size > updateList.size) { + updateToc() + } + } + + + fun checkCount() { + val count = App.INSTANCE.getPrefInt(PreferKey.installCount, 0) + App.INSTANCE.putPrefInt(PreferKey.installCount, count + 1) + + if (App.INSTANCE.getPrefLong(PreferKey.installTime) == 0L) { + App.INSTANCE.putPrefLong(PreferKey.installTime, System.currentTimeMillis()) + } else { + if (StringUtils.checkInstallTime() && !App.INSTANCE.getPrefBoolean(PreferKey.appraiseShow)) { + showEvaluate.postValue(true) + App.INSTANCE.putPrefBoolean(PreferKey.appraiseShow, true) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/about/AboutActivity.kt b/app/src/main/java/com/novel/read/ui/about/AboutActivity.kt new file mode 100644 index 0000000..c95a27c --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/about/AboutActivity.kt @@ -0,0 +1,11 @@ +package com.novel.read.ui.about + +import android.os.Bundle +import com.novel.read.R +import com.novel.read.base.BaseActivity + +class AboutActivity : BaseActivity(R.layout.activity_about) { + override fun onActivityCreated(savedInstanceState: Bundle?) { + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/channel/ChannelActivity.kt b/app/src/main/java/com/novel/read/ui/channel/ChannelActivity.kt new file mode 100644 index 0000000..ed0b729 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/channel/ChannelActivity.kt @@ -0,0 +1,77 @@ +package com.novel.read.ui.channel + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.observe +import androidx.recyclerview.widget.GridLayoutManager +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.data.model.ChannelSection +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_channel.* +import java.util.ArrayList + +class ChannelActivity : VMBaseActivity(R.layout.activity_channel) { + + override val viewModel: ChannelViewModel + get() = getViewModel(ChannelViewModel::class.java) + + private lateinit var adapter: ChannelAdapter + private val data: MutableList = ArrayList() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(rlv_channel) + initRecyclerView() + upRecyclerData() + } + + private fun initRecyclerView() { + rlv_channel.layoutManager = GridLayoutManager(this, 2) + adapter = ChannelAdapter() + rlv_channel.adapter = adapter + } + + private fun upRecyclerData() { + onRefresh() + viewModel.run { + channelResp.observe(this@ChannelActivity) { + for (i in it.allType.indices) { + data.add(ChannelSection(false, it.allType[i])) + } + adapter.setList(data) + } + + refreshStatus.observe(this@ChannelActivity) { + when (it) { + AppConst.loading -> { + adapter.setList(null) + adapter.isUseEmpty = true + } + AppConst.complete -> { + adapter.isUseEmpty = false + } + else -> { + adapter.setList(null) + adapter.setEmptyView(getErrorView()) + adapter.isUseEmpty = true + } + } + } + } + + } + + private fun onRefresh() { + adapter.setEmptyView(R.layout.view_loading) + viewModel.getChannel() + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_channel, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/channel/ChannelAdapter.kt b/app/src/main/java/com/novel/read/ui/channel/ChannelAdapter.kt new file mode 100644 index 0000000..fc4ba52 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/channel/ChannelAdapter.kt @@ -0,0 +1,44 @@ +package com.novel.read.ui.channel + +import android.annotation.SuppressLint +import com.chad.library.adapter.base.BaseSectionQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.constant.IntentAction +import com.novel.read.data.model.AllType +import com.novel.read.data.model.ChannelSection +import kotlinx.android.synthetic.main.item_channel.view.* +import kotlinx.android.synthetic.main.item_channel_head.view.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.startActivity + +class ChannelAdapter : BaseSectionQuickAdapter( + R.layout.item_channel_head, + R.layout.item_channel +) { + + override fun convertHeader(helper: BaseViewHolder, item: ChannelSection) { + val value = item.obj as String + helper.itemView.run { + tv_sex.text = value + } + } + + @SuppressLint("SetTextI18n") + override fun convert(holder: BaseViewHolder, item: ChannelSection) { + val value = item.obj as AllType + holder.itemView.run { + tv_name.text = value.getBChannel() + iv_cover.load(value.typeImageUrl, "", "") + tv_count.text= "${value.bookCount}本" + onClick { + context.startActivity( + Pair(IntentAction.bookTypeId, value.bookTypeId), + Pair(IntentAction.channelName, value.getBChannel()), + ) + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/channel/ChannelInfoActivity.kt b/app/src/main/java/com/novel/read/ui/channel/ChannelInfoActivity.kt new file mode 100644 index 0000000..24ff2d8 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/channel/ChannelInfoActivity.kt @@ -0,0 +1,96 @@ +package com.novel.read.ui.channel + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.listener.OnLoadMoreListener +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.constant.IntentAction +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_channel_info.* + +class ChannelInfoActivity : VMBaseActivity(R.layout.activity_channel_info) { + override val viewModel: ChannelInfoViewModel + get() = getViewModel(ChannelInfoViewModel::class.java) + private lateinit var adapter: ChannelInfoAdapter + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(rlv_book) + initRecyclerView() + viewModel.initData(intent) + upRecyclerData() + initLoadMore() + } + + private fun initRecyclerView() { + rlv_book.layoutManager = LinearLayoutManager(this) + adapter = ChannelInfoAdapter() + rlv_book.adapter = adapter + } + + private fun upRecyclerData() { + val channelName = intent.getStringExtra(IntentAction.channelName) ?: "" + title_bar.title = channelName + onRefresh() + viewModel.run { + bookListResp.observe(this@ChannelInfoActivity) { + adapter.setList(it) + } + pageStatus.observe(this@ChannelInfoActivity) { + when (it) { + AppConst.loading -> { + adapter.isUseEmpty = true + } + AppConst.complete -> { + adapter.isUseEmpty = false + } + AppConst.loadMore -> { + adapter.loadMoreModule.isEnableLoadMore = true + } + AppConst.loadComplete -> { + adapter.loadMoreModule.loadMoreComplete() + } + AppConst.noMore -> { + adapter.loadMoreModule.loadMoreEnd() + } + AppConst.loadMoreFail -> { + adapter.loadMoreModule.loadMoreFail() + } + else -> { + adapter.setList(null) + adapter.setEmptyView(getErrorView()) + adapter.isUseEmpty = true + } + } + } + } + } + + private fun initLoadMore() { + adapter.loadMoreModule.setOnLoadMoreListener(object : OnLoadMoreListener { + override fun onLoadMore() { + if (viewModel.pageStatus.value == AppConst.loadMore) { + return + } + viewModel.loadMore() + } + }) + adapter.loadMoreModule.isAutoLoadMore = true + adapter.loadMoreModule.isEnableLoadMoreIfNotFullPage=false + } + + private fun onRefresh() { + adapter.setEmptyView(R.layout.view_loading) + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_book, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/channel/ChannelInfoAdapter.kt b/app/src/main/java/com/novel/read/ui/channel/ChannelInfoAdapter.kt new file mode 100644 index 0000000..a3e6e2b --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/channel/ChannelInfoAdapter.kt @@ -0,0 +1,28 @@ +package com.novel.read.ui.channel + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.BookListResp +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_book_common.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class ChannelInfoAdapter : + BaseQuickAdapter(R.layout.item_book_common), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: BookListResp) { + holder.itemView.run { + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_book_name.text = item.getBBookName() + tv_book_description.text = item.getBIntroduction() + tv_book_author.text = item.getBAuthorName() + tv_word.text = context.getString(R.string.book_word, item.wordCount / 10000) + tv_category.text = item.getBCategoryName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/channel/ChannelInfoViewModel.kt b/app/src/main/java/com/novel/read/ui/channel/ChannelInfoViewModel.kt new file mode 100644 index 0000000..d0990c4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/channel/ChannelInfoViewModel.kt @@ -0,0 +1,58 @@ +package com.novel.read.ui.channel + +import android.app.Application +import android.content.Intent +import android.util.Log +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.constant.IntentAction +import com.novel.read.data.model.BookListResp +import com.novel.read.network.repository.BookRepository + +class ChannelInfoViewModel(application: Application) : BaseViewModel(application) { + + private val bookRepository by lazy { BookRepository() } + var bookListResp = MutableLiveData>() + private lateinit var mList: MutableList + val pageStatus = MutableLiveData() + var page: Int = 1 + private var pageSize: Int = 20 + private var channelId: Long = 0L + private var total: Int = 0 + + fun initData(intent: Intent) { + channelId = intent.getLongExtra(IntentAction.bookTypeId, 0L) + launch( + block = { + pageStatus.value = AppConst.loading + val data = bookRepository.getBookList(channelId, page, pageSize) + total = data.count + mList = data.bookList + bookListResp.value = mList + pageStatus.value = AppConst.complete + }, + error = { + pageStatus.value = AppConst.error + } + ) + } + + fun loadMore() { + Log.e("loadMore", "loadMore: $page") + launch(block = { + pageStatus.value = AppConst.loadMore + page++ + val mData = bookRepository.getBookList(channelId, page, pageSize).bookList + mList.addAll(mData) + bookListResp.value = mList + pageStatus.value = AppConst.loadComplete + if (pageSize > mData.size) { + pageStatus.value = AppConst.noMore + } + }, error = { + page-- + pageStatus.value = AppConst.loadMoreFail + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/channel/ChannelViewModel.kt b/app/src/main/java/com/novel/read/ui/channel/ChannelViewModel.kt new file mode 100644 index 0000000..19157c5 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/channel/ChannelViewModel.kt @@ -0,0 +1,24 @@ +package com.novel.read.ui.channel + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.data.model.ChannelResp +import com.novel.read.network.repository.BookRepository + +class ChannelViewModel(application: Application) : BaseViewModel(application) { + + private val bookRepository by lazy { BookRepository() } + var channelResp = MutableLiveData() + val refreshStatus = MutableLiveData() + fun getChannel(){ + refreshStatus.value = AppConst.loading + launch({ + channelResp.value = bookRepository.getChannelList() + refreshStatus.value = AppConst.complete + }, { + refreshStatus.value = AppConst.error + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/chapter/BookMarkAdapter.kt b/app/src/main/java/com/novel/read/ui/chapter/BookMarkAdapter.kt new file mode 100644 index 0000000..2fc63ab --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/chapter/BookMarkAdapter.kt @@ -0,0 +1,19 @@ +package com.novel.read.ui.chapter + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.db.entity.Bookmark +import kotlinx.android.synthetic.main.item_bookmark.view.* + +class BookMarkAdapter :BaseQuickAdapter(R.layout.item_bookmark) { + + override fun convert(holder: BaseViewHolder, item: Bookmark) { + holder.itemView.run { + tv_chapter_name.text = item.chapterName + tv_content.text = item.content + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/chapter/BookmarkFragment.kt b/app/src/main/java/com/novel/read/ui/chapter/BookmarkFragment.kt new file mode 100644 index 0000000..6aba0b5 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/chapter/BookmarkFragment.kt @@ -0,0 +1,112 @@ +package com.novel.read.ui.chapter + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.EditText +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.VMBaseFragment +import com.novel.read.data.db.entity.Bookmark +import com.novel.read.lib.ATH +import com.novel.read.lib.dialogs.alert +import com.novel.read.lib.dialogs.customView +import com.novel.read.lib.dialogs.noButton +import com.novel.read.lib.dialogs.yesButton +import com.novel.read.ui.widget.VerticalDivider +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.dialog_edit_text.view.* +import kotlinx.android.synthetic.main.fragment_bookmark.* + +class BookmarkFragment : VMBaseFragment(R.layout.fragment_bookmark), + ChapterListViewModel.BookmarkCallBack { + + override val viewModel: ChapterListViewModel + get() = getViewModelOfActivity(ChapterListViewModel::class.java) + + private lateinit var adapter: BookMarkAdapter + private var bookmarkLiveData = MutableLiveData>() + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.bookMarkCallBack = this + initRecyclerView() + initData() + } + + private fun initRecyclerView() { + ATH.applyEdgeEffectColor(recycler_view) + adapter = BookMarkAdapter() + recycler_view.layoutManager = LinearLayoutManager(requireContext()) + recycler_view.addItemDecoration(VerticalDivider(requireContext())) + recycler_view.adapter = adapter + } + + private fun initData() { + viewModel.book?.let { book -> + bookmarkLiveData.removeObservers(viewLifecycleOwner) + bookmarkLiveData.value = App.db.getBookMarkDao().observeByBook(book.bookId) + bookmarkLiveData.observe(viewLifecycleOwner) { + adapter.setList(it) + } + } + + adapter.setOnItemClickListener { adapter, view, position -> + val bookmarkData = Intent() + val bookmark: Bookmark = bookmarkLiveData.value!![position] + bookmarkData.putExtra("index", bookmark.chapterIndex) + bookmarkData.putExtra("pageIndex", bookmark.pageIndex) + activity?.setResult(Activity.RESULT_OK, bookmarkData) + activity?.finish() + } + + adapter.setOnItemLongClickListener { adapter, view, position -> + val bookmark: Bookmark = bookmarkLiveData.value!![position] + viewModel.book?.let { book -> + requireContext().alert(R.string.bookmark) { + var editText: EditText? = null + message = book.bookName + " • " + bookmark.chapterName + customView { + layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { + editText = edit_view.apply { + setHint(R.string.note_content) + setText(bookmark.content) + } + } + } + yesButton { + editText?.text?.toString()?.let { editContent -> + bookmark.content = editContent + App.db.getBookMarkDao().update(bookmark) + bookmarkLiveData.value=App.db.getBookMarkDao().observeByBook(book.bookId) + adapter.notifyDataSetChanged() + } + } + noButton() + neutralButton(R.string.delete) { + App.db.getBookMarkDao().delete(bookmark) + bookmarkLiveData.value=App.db.getBookMarkDao().observeByBook(book.bookId) + adapter.notifyDataSetChanged() + } + }.show().applyTint().requestInputMethod() + } + true + } + } + + override fun startBookmarkSearch(newText: String?) { + if (newText.isNullOrBlank()) { + initData() + } else { + bookmarkLiveData.removeObservers(viewLifecycleOwner) + bookmarkLiveData.value = + App.db.getBookMarkDao().liveDataSearch( + viewModel.bookId, + newText + ) + bookmarkLiveData.observe(viewLifecycleOwner, { adapter.setList(it) }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/chapter/ChapterListActivity.kt b/app/src/main/java/com/novel/read/ui/chapter/ChapterListActivity.kt new file mode 100644 index 0000000..763c118 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/chapter/ChapterListActivity.kt @@ -0,0 +1,94 @@ +package com.novel.read.ui.chapter + +import android.os.Bundle +import android.view.Menu +import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.activity_chapter_list.* +import kotlinx.android.synthetic.main.view_tab_layout.* + + +class ChapterListActivity : VMBaseActivity(R.layout.activity_chapter_list) { + override val viewModel: ChapterListViewModel + get() = getViewModel(ChapterListViewModel::class.java) + + private var searchView: SearchView? = null + + override fun onActivityCreated(savedInstanceState: Bundle?) { + tab_layout.isTabIndicatorFullWidth = false + tab_layout.setSelectedTabIndicatorColor(accentColor) + intent.getStringExtra("bookId")?.let { + viewModel.initBook(it) { + view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager) + tab_layout.setupWithViewPager(view_pager) + } + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.search_view, menu) + val search = menu.findItem(R.id.menu_search) + searchView = search.actionView as SearchView + ATH.setTint(searchView!!, primaryTextColor) + searchView?.maxWidth = resources.displayMetrics.widthPixels + searchView?.onActionViewCollapsed() + searchView?.setOnCloseListener { + tab_layout.visible() + false + } + searchView?.setOnSearchClickListener { tab_layout.gone() } + searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return false + } + + override fun onQueryTextChange(newText: String): Boolean { + if (tab_layout.selectedTabPosition == 1) { + viewModel.startBookmarkSearch(newText) + } else { + viewModel.startChapterListSearch(newText) + } + return false + } + }) + return super.onCompatCreateOptionsMenu(menu) + } + + private inner class TabFragmentPageAdapter(fm: FragmentManager) : + FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getItem(position: Int): Fragment { + return when (position) { + 1 -> BookmarkFragment() + else -> ChapterListFragment() + } + } + + override fun getCount(): Int { + return 2 + } + + override fun getPageTitle(position: Int): CharSequence? { + return when (position) { + 1 -> getString(R.string.bookmark) + else -> getString(R.string.chapter_list) + } + } + + } + + override fun onBackPressed() { + if (tab_layout.isGone) { + searchView?.onActionViewCollapsed() + tab_layout.visible() + } else { + super.onBackPressed() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/chapter/ChapterListAdapter.kt b/app/src/main/java/com/novel/read/ui/chapter/ChapterListAdapter.kt new file mode 100644 index 0000000..fc4bf26 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/chapter/ChapterListAdapter.kt @@ -0,0 +1,57 @@ +package com.novel.read.ui.chapter + +import android.view.View +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.help.BookHelp +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.item_chapter_list.view.* + +class ChapterListAdapter(val callback: Callback) : + BaseQuickAdapter(R.layout.item_chapter_list) { + + val cacheFileNames = hashSetOf() + + override fun convert(holder: BaseViewHolder, item: BookChapter) { + holder.itemView.run { + val isDur = callback.durChapterIndex() == item.chapterIndex + val cached = callback.isLocalBook + || cacheFileNames.contains(BookHelp.formatChapterName(item)) + if (isDur) { + tv_chapter_name.setTextColor(context.accentColor) + } else { + tv_chapter_name.setTextColor(context.getCompatColor(R.color.primaryText)) + } + tv_chapter_name.text = item.chapterName + if (!item.updateDate.isNullOrEmpty()) { + tv_tag.text = item.updateDate + tv_tag.visible() + } + upHasCache(this, isDur, cached) + + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callback.openChapter(it) + } + } + } + } + + private fun upHasCache(itemView: View, isDur: Boolean, cached: Boolean) = itemView.apply { + tv_chapter_name.paint.isFakeBoldText = cached + iv_checked.setImageResource(R.drawable.ic_outline_cloud_24) + iv_checked.visible(!cached) + if (isDur) { + iv_checked.setImageResource(R.drawable.ic_check) + iv_checked.visible() + } + } + + interface Callback { + val isLocalBook: Boolean + fun openChapter(bookChapter: BookChapter) + fun durChapterIndex(): Int + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/chapter/ChapterListFragment.kt b/app/src/main/java/com/novel/read/ui/chapter/ChapterListFragment.kt new file mode 100644 index 0000000..981f146 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/chapter/ChapterListFragment.kt @@ -0,0 +1,141 @@ +package com.novel.read.ui.chapter + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.observe +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.VMBaseFragment +import com.novel.read.constant.EventBus +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.help.BookHelp +import com.novel.read.ui.widget.UpLinearLayoutManager +import com.novel.read.ui.widget.VerticalDivider +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.fragment_chapter_list.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.anko.sdk27.listeners.onClick + +class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapter_list), + ChapterListViewModel.ChapterListCallBack,ChapterListAdapter.Callback { + + lateinit var adapter: ChapterListAdapter + private var durChapterIndex = 0 + private lateinit var mLayoutManager: UpLinearLayoutManager + private var tocLiveData: MutableLiveData>? = MutableLiveData() + private var scrollToDurChapter = false + + override val viewModel: ChapterListViewModel + get() = getViewModelOfActivity(ChapterListViewModel::class.java) + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.chapterCallBack = this + val bbg = bottomBackground + val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg)) + ll_chapter_base_info.setBackgroundColor(bbg) + tv_current_chapter_info.setTextColor(btc) + iv_chapter_top.setColorFilter(btc) + iv_chapter_bottom.setColorFilter(btc) + initRecyclerView() + initView() + initBook() + } + + private fun initRecyclerView() { + adapter = ChapterListAdapter(this) + mLayoutManager = UpLinearLayoutManager(requireContext()) + recycler_view.layoutManager = mLayoutManager + recycler_view.addItemDecoration(VerticalDivider(requireContext())) + recycler_view.adapter = adapter + } + + private fun initView() { + iv_chapter_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) } + iv_chapter_bottom.onClick { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + tv_current_chapter_info.onClick { + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + } + + } + + @SuppressLint("SetTextI18n") + private fun initBook() { + launch { + initDoc() + viewModel.book?.let { + durChapterIndex = it.durChapterIndex + tv_current_chapter_info.text = + "${it.durChapterTitle}(${it.durChapterIndex + 1}/${tocLiveData?.value?.size})" + initCacheFileNames(it) + } + } + } + + private fun initDoc() { + tocLiveData?.removeObservers(this@ChapterListFragment) + tocLiveData?.value = App.db.getChapterDao().observeByBook(viewModel.bookId) + tocLiveData?.observe(viewLifecycleOwner) { + adapter.setList(it) + if (!scrollToDurChapter) { + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + scrollToDurChapter = true + } + } + } + + private fun initCacheFileNames(book: Book) { + launch(Dispatchers.IO) { + adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + withContext(Dispatchers.Main) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SAVE_CONTENT) { chapter -> + viewModel.book?.bookId?.let { bookId -> + if (chapter.bookId == bookId) { + adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter)) + adapter.notifyItemChanged(chapter.chapterIndex, true) + } + } + } + } + + override fun startChapterListSearch(newText: String?) { + if (newText.isNullOrBlank()) { + initDoc() + } else { + tocLiveData?.removeObservers(this) + tocLiveData?.value = App.db.getChapterDao().liveDataSearch(viewModel.bookId, newText) + tocLiveData?.observe(viewLifecycleOwner, { + adapter.setList(it) + }) + } + } + + override val isLocalBook: Boolean + get() = viewModel.book?.isLocalBook() == true + + override fun durChapterIndex(): Int { + return durChapterIndex + } + + override fun openChapter(bookChapter: BookChapter) { + activity?.setResult(Activity.RESULT_OK, Intent().putExtra("index", bookChapter.chapterIndex)) + activity?.finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/chapter/ChapterListViewModel.kt b/app/src/main/java/com/novel/read/ui/chapter/ChapterListViewModel.kt new file mode 100644 index 0000000..932bf30 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/chapter/ChapterListViewModel.kt @@ -0,0 +1,38 @@ +package com.novel.read.ui.chapter + +import android.app.Application +import com.novel.read.App +import com.novel.read.base.BaseViewModel +import com.novel.read.data.db.entity.Book + +class ChapterListViewModel(application: Application) : BaseViewModel(application) { + var bookId: String = "" + var book: Book? = null + var chapterCallBack: ChapterListCallBack? = null + var bookMarkCallBack: BookmarkCallBack? = null + + fun initBook(bookId: String, success: () -> Unit) { + this.bookId = bookId + execute { + book = App.db.getBookDao().getBook(bookId) + }.onSuccess { + success.invoke() + } + } + + fun startChapterListSearch(newText: String?) { + chapterCallBack?.startChapterListSearch(newText) + } + + fun startBookmarkSearch(newText: String?) { + bookMarkCallBack?.startBookmarkSearch(newText) + } + + interface ChapterListCallBack { + fun startChapterListSearch(newText: String?) + } + + interface BookmarkCallBack { + fun startBookmarkSearch(newText: String?) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/daily/DailyActivity.kt b/app/src/main/java/com/novel/read/ui/daily/DailyActivity.kt new file mode 100644 index 0000000..a66f412 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/daily/DailyActivity.kt @@ -0,0 +1,95 @@ +package com.novel.read.ui.daily + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.listener.OnLoadMoreListener +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_daily.* + +class DailyActivity : VMBaseActivity(R.layout.activity_daily) { + + override val viewModel: DailyViewModel + get() = getViewModel(DailyViewModel::class.java) + + private lateinit var adapter: DailyAdapter + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(rlv_daily) + initRecyclerView() + upRecyclerData() + initLoadMore() + } + + private fun initRecyclerView() { + rlv_daily.layoutManager = LinearLayoutManager(this) + adapter = DailyAdapter() + rlv_daily.adapter = adapter + } + + private fun upRecyclerData() { + onRefresh() + viewModel.run { + bookListResp.observe(this@DailyActivity) { + adapter.setList(it) + } + pageStatus.observe(this@DailyActivity) { + when (it) { + AppConst.loading -> { + adapter.isUseEmpty = true + } + AppConst.complete -> { + adapter.isUseEmpty = false + } + AppConst.loadMore -> { + adapter.loadMoreModule.isEnableLoadMore = true + } + AppConst.loadComplete -> { + adapter.loadMoreModule.loadMoreComplete() + } + AppConst.noMore -> { + adapter.loadMoreModule.loadMoreEnd() + } + AppConst.loadMoreFail -> { + adapter.loadMoreModule.loadMoreFail() + } + else -> { + adapter.setList(null) + adapter.setEmptyView(getErrorView()) + adapter.isUseEmpty = true + } + } + } + } + } + + private fun initLoadMore() { + adapter.loadMoreModule.setOnLoadMoreListener(object : OnLoadMoreListener { + override fun onLoadMore() { + if (viewModel.pageStatus.value == AppConst.loadMore) { + return + } + viewModel.loadMore() + } + }) + adapter.loadMoreModule.isAutoLoadMore = true + adapter.loadMoreModule.isEnableLoadMoreIfNotFullPage=false + } + + private fun onRefresh() { + adapter.setEmptyView(R.layout.view_loading) + viewModel.initData() + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_daily, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/daily/DailyAdapter.kt b/app/src/main/java/com/novel/read/ui/daily/DailyAdapter.kt new file mode 100644 index 0000000..4ac2bec --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/daily/DailyAdapter.kt @@ -0,0 +1,28 @@ +package com.novel.read.ui.daily + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.BookListResp +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_book_common.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class DailyAdapter : + BaseQuickAdapter(R.layout.item_book_common), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: BookListResp) { + holder.itemView.run { + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_book_name.text = item.getBBookName() + tv_book_description.text = item.getBIntroduction() + tv_book_author.text = item.getBAuthorName() + tv_word.text = context.getString(R.string.book_word, item.wordCount / 10000) + tv_category.text = item.getBCategoryName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/daily/DailyViewModel.kt b/app/src/main/java/com/novel/read/ui/daily/DailyViewModel.kt new file mode 100644 index 0000000..ee18cda --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/daily/DailyViewModel.kt @@ -0,0 +1,51 @@ +package com.novel.read.ui.daily + +import android.app.Application +import android.util.Log +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.constant.LayoutType +import com.novel.read.data.model.BookListResp +import com.novel.read.network.repository.BookRepository + +class DailyViewModel(application: Application) : BaseViewModel(application) { + + private val bookRepository by lazy { BookRepository() } + var bookListResp = MutableLiveData>() + val pageStatus = MutableLiveData() + private lateinit var mList: MutableList + var page: Int = 1 + private var pageSize: Int = 20 + + fun initData() { + launch(block = { + pageStatus.value = AppConst.loading + val data = bookRepository.getBooRank(page, pageSize, LayoutType.RECOMMEND) + mList = data.bookRankList + bookListResp.value = mList + pageStatus.value = AppConst.complete + }, error = { + pageStatus.value = AppConst.error + }) + } + + fun loadMore() { + Log.e("loadMore", "loadMore: $page") + launch(block = { + pageStatus.value = AppConst.loadMore + page++ +// val mData = bookRepository.getDailyList(page, pageSize).bookList + val mData = bookRepository.getBooRank(page, pageSize, LayoutType.RECOMMEND).bookRankList + mList.addAll(mData) + bookListResp.value = mList + pageStatus.value = AppConst.loadComplete + if (pageSize > mData.size) { + pageStatus.value = AppConst.noMore + } + },error = { + page-- + pageStatus.value = AppConst.loadMoreFail + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/end/EndActivity.kt b/app/src/main/java/com/novel/read/ui/end/EndActivity.kt new file mode 100644 index 0000000..3e8817b --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/end/EndActivity.kt @@ -0,0 +1,96 @@ +package com.novel.read.ui.end + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.listener.OnLoadMoreListener +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_end.* + +class EndActivity : VMBaseActivity(R.layout.activity_end) { + + override val viewModel: EndViewModel + get() = getViewModel(EndViewModel::class.java) + + private lateinit var adapter: EndAdapter + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(rlv_end) + initRecyclerView() + upRecyclerData() + initLoadMore() + } + + private fun initRecyclerView() { + rlv_end.layoutManager = LinearLayoutManager(this) + adapter = EndAdapter() + rlv_end.adapter = adapter + } + + private fun upRecyclerData() { + onRefresh() + viewModel.run { + bookListResp.observe(this@EndActivity) { + adapter.setList(it) + } + pageStatus.observe(this@EndActivity) { + when (it) { + AppConst.loading -> { + adapter.isUseEmpty = true + } + AppConst.complete -> { + adapter.isUseEmpty = false + } + AppConst.loadMore -> { + adapter.loadMoreModule.isEnableLoadMore = true + } + AppConst.loadComplete -> { + adapter.loadMoreModule.loadMoreComplete() + } + AppConst.noMore -> { + adapter.loadMoreModule.loadMoreEnd() + } + AppConst.loadMoreFail -> { + adapter.loadMoreModule.loadMoreFail() + } + else -> { + adapter.setList(null) + adapter.setEmptyView(getErrorView()) + adapter.isUseEmpty = true + } + } + } + } + } + + private fun initLoadMore() { + adapter.loadMoreModule.setOnLoadMoreListener(object : OnLoadMoreListener { + override fun onLoadMore() { + if (viewModel.pageStatus.value == AppConst.loadMore) { + return + } + viewModel.loadMore() + } + }) + adapter.loadMoreModule.isAutoLoadMore = true + adapter.loadMoreModule.isEnableLoadMoreIfNotFullPage = false + } + + private fun onRefresh() { + adapter.setEmptyView(R.layout.view_loading) + viewModel.initData() + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_end, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/end/EndAdapter.kt b/app/src/main/java/com/novel/read/ui/end/EndAdapter.kt new file mode 100644 index 0000000..f6258c4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/end/EndAdapter.kt @@ -0,0 +1,28 @@ +package com.novel.read.ui.end + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.BookListResp +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_book_common.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class EndAdapter : + BaseQuickAdapter(R.layout.item_book_common), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: BookListResp) { + holder.itemView.run { + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_book_name.text = item.getBBookName() + tv_book_description.text = item.getBIntroduction() + tv_book_author.text = item.getBAuthorName() + tv_word.text = context.getString(R.string.book_word, item.wordCount / 10000) + tv_category.text = item.getBCategoryName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/end/EndViewModel.kt b/app/src/main/java/com/novel/read/ui/end/EndViewModel.kt new file mode 100644 index 0000000..ec4ed66 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/end/EndViewModel.kt @@ -0,0 +1,49 @@ +package com.novel.read.ui.end + +import android.app.Application +import android.util.Log +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.data.model.BookListResp +import com.novel.read.network.repository.BookRepository + +class EndViewModel(application: Application) : BaseViewModel(application) { + + private val bookRepository by lazy { BookRepository() } + var bookListResp = MutableLiveData>() + private lateinit var mList: MutableList + val pageStatus = MutableLiveData() + var page: Int = 1 + private var pageSize: Int = 20 + + fun initData() { + launch(block = { + pageStatus.value = AppConst.loading + val data = bookRepository.getEndList(page, pageSize) + mList = data.bookList + bookListResp.value = mList + pageStatus.value = AppConst.complete + }, error = { + pageStatus.value = AppConst.error + }) + } + + fun loadMore() { + Log.e("loadMore", "loadMore: $page") + launch(block = { + pageStatus.value = AppConst.loadMore + page++ + val mData = bookRepository.getEndList(page, pageSize).bookList + mList.addAll(mData) + bookListResp.value = mList + pageStatus.value = AppConst.loadComplete + if (pageSize > mData.size) { + pageStatus.value = AppConst.noMore + } + }, error = { + page-- + pageStatus.value = AppConst.loadMoreFail + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/feedback/FeedBackActivity.kt b/app/src/main/java/com/novel/read/ui/feedback/FeedBackActivity.kt new file mode 100644 index 0000000..9217923 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/feedback/FeedBackActivity.kt @@ -0,0 +1,35 @@ +package com.novel.read.ui.feedback + +import android.os.Bundle +import android.text.TextUtils +import androidx.lifecycle.observe +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_feed_back.* +import org.jetbrains.anko.toast + +class FeedBackActivity : VMBaseActivity(R.layout.activity_feed_back) { + + override val viewModel: FeedBackViewModel + get() = getViewModel(FeedBackViewModel::class.java) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + button_report.setOnClickListener { + if (TextUtils.isEmpty(et_feed.text.toString().trim())){ + toast(getString(R.string.feedback_not_empty)) + return@setOnClickListener + } + viewModel.feedback(et_feed.text.toString().trim()) + } + + viewModel.success.observe(this){ + if(it){ + toast(getString(R.string.feedback_success)) + finish() + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/feedback/FeedBackViewModel.kt b/app/src/main/java/com/novel/read/ui/feedback/FeedBackViewModel.kt new file mode 100644 index 0000000..71bf47d --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/feedback/FeedBackViewModel.kt @@ -0,0 +1,19 @@ +package com.novel.read.ui.feedback + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.network.repository.BookRepository + +class FeedBackViewModel(application: Application) : BaseViewModel(application) { + + private val bookRepository by lazy { BookRepository() } + var success = MutableLiveData() + + fun feedback(content: String) { + launch(block = { + bookRepository.feedback(content) + success.postValue(true) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/info/BookInfoActivity.kt b/app/src/main/java/com/novel/read/ui/info/BookInfoActivity.kt new file mode 100644 index 0000000..02b3d13 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/info/BookInfoActivity.kt @@ -0,0 +1,167 @@ +package com.novel.read.ui.info + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.lifecycle.observe +import androidx.recyclerview.widget.GridLayoutManager +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.constant.EventBus +import com.novel.read.constant.IntentAction +import com.novel.read.data.db.entity.Book +import com.novel.read.help.AppConfig +import com.novel.read.help.IntentDataHelp +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.activity_book_info.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.startActivity +import org.jetbrains.anko.startActivityForResult + +class BookInfoActivity : VMBaseActivity(R.layout.activity_book_info) { + + private lateinit var adapter: BookInfoAdapter + private val requestCodeRead = 432 + + override val viewModel: BookInfoViewModel + get() = getViewModel(BookInfoViewModel::class.java) + + companion object { + fun actionBookInfo(context: Context, bookId: Long, bookTypeId: Int?) { + context.startActivity( + Pair(IntentAction.bookId, bookId), + Pair(IntentAction.bookTypeId, bookTypeId), + ) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.initData(intent) + viewModel.getRecommend(intent) + tv_shelf.setTextColor(getPrimaryTextColor(ColorUtils.isColorLight(bottomBackground))) + initRecycleView() + initData() + initClick() + } + + private fun initRecycleView() { + rlv_recommend.layoutManager = GridLayoutManager(this, 4) + adapter = BookInfoAdapter() + rlv_recommend.adapter = adapter + } + + private fun initData() { + val dividerColor = if (AppConfig.isNightTheme) { + ColorUtils.shiftColor(backgroundColor, 1.05f) + } else { + ColorUtils.shiftColor(backgroundColor, 0.95f) + } + preference_divider_above.setBackgroundColor(dividerColor) + viewModel.bookResp.observe(this) { + iv_book.load(it.coverImageUrl, it.bookName, it.authorName) + tv_book_name.text = it.bookName + tv_book_author.text = it.authorName + tv_key.text = it.lastUpdateChapterDate + tv_status.text = it.categoryName + tv_word.text = getString(R.string.book_word, it.wordCount / 10000) + + tv_tro.text = it.introduction + + upTvBookshelf() + } + viewModel.bookListResp.observe(this) { + adapter.setList(it) + } + viewModel.status.observe(this) { + when (it) { + AppConst.loading -> { + refresh.showLoading() + } + AppConst.complete -> { + refresh.showFinish() + } + else -> { + refresh.showError() + } + } + } + } + + private fun initClick() { + tv_shelf.onClick { + if (viewModel.inBookshelf) { + deleteBook() + } else { + viewModel.addToBookshelf { + upTvBookshelf() + } + } + + } + + tv_read.onClick { + viewModel.bookData.value?.let { + readBook(it) + } + + } + + refresh.setOnReloadingListener { + viewModel.initData(intent) + viewModel.getRecommend(intent) + } + } + + private fun readBook(book: Book) { + if (!viewModel.inBookshelf) { + startReadActivity(book) + } else { + viewModel.saveBook { + startReadActivity(book) + } + } + postEvent(EventBus.UP_BOOK, 0L) + } + + private fun startReadActivity(book: Book) { + startActivityForResult( + requestCodeRead, + Pair("bookId", book.bookId.toString()), + Pair("inBookshelf", viewModel.inBookshelf), + Pair("key", IntentDataHelp.putData(book)) + ) + } + + private fun upTvBookshelf() { + if (viewModel.inBookshelf) { + tv_shelf.text = getString(R.string.remove_from_bookshelf) + } else { + tv_shelf.text = getString(R.string.add_to_shelf) + } + postEvent(EventBus.UP_BOOK, 0L) + } + + @SuppressLint("InflateParams") + private fun deleteBook() { + viewModel.delBook { + upTvBookshelf() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + requestCodeRead -> if (resultCode == Activity.RESULT_OK) { + viewModel.inBookshelf = true + viewModel.saveBook { + upTvBookshelf() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/info/BookInfoAdapter.kt b/app/src/main/java/com/novel/read/ui/info/BookInfoAdapter.kt new file mode 100644 index 0000000..cda50e8 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/info/BookInfoAdapter.kt @@ -0,0 +1,23 @@ +package com.novel.read.ui.info + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.BookListResp +import com.novel.read.lib.ATH +import kotlinx.android.synthetic.main.item_hot_item.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class BookInfoAdapter :BaseQuickAdapter(R.layout.item_hot_item) { + + override fun convert(holder: BaseViewHolder, item: BookListResp) { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_name.text = item.getBBookName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/info/BookInfoViewModel.kt b/app/src/main/java/com/novel/read/ui/info/BookInfoViewModel.kt new file mode 100644 index 0000000..41ed5f0 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/info/BookInfoViewModel.kt @@ -0,0 +1,142 @@ +package com.novel.read.ui.info + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import com.novel.read.App +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.constant.IntentAction +import com.novel.read.data.db.entity.Book +import com.novel.read.data.model.BookListResp +import com.novel.read.data.model.BookResp +import com.novel.read.network.repository.BookRepository +import com.novel.read.service.help.ReadBook + +class BookInfoViewModel(application: Application) : BaseViewModel(application) { + + var bookResp = MutableLiveData() + val bookData = MutableLiveData() + var status = MutableLiveData() + var inBookshelf = false + private val bookRepository by lazy { BookRepository() } + var bookListResp = MutableLiveData>() + private lateinit var mList: MutableList + private var bookTypeId: Int = 0 + + private var durChapterTime: Long = 0 + private var durChapterIndex: Int = 0 + private var durChapterPos: Int = 0 + private var durChapterTitle: String? = "" + private var totalChapterNum: Int = 0 + + fun initData(intent: Intent) { + + val bookId = intent.getLongExtra(IntentAction.bookId,0L) + + App.db.getBookDao().getBook(bookId.toString())?.let { book -> + inBookshelf = true + durChapterTime = book.durChapterTime + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + totalChapterNum = book.totalChapterNum + durChapterTitle = book.durChapterTitle + } + launch( + block = { + status.postValue(AppConst.loading) + val data = bookRepository.getBookInfo(bookId).book + bookResp.postValue(data) + val book = Book( + authorPenname = data.authorName, + bookId = data.bookId, + bookName = data.bookName, + bookStatus = "01", + categoryName = data.categoryName, + channelName = data.categoryName, + cName = "轻小说", + coverImageUrl = data.coverImageUrl, + introduction = data.introduction, + keyWord = data.keyWord, + lastUpdateChapterDate = data.lastUpdateChapterDate, + status = 0, + wordCount = data.wordCount, + bookTypeId = data.bookTypeId + ) + book.durChapterTime = durChapterTime + book.durChapterIndex = durChapterIndex + book.durChapterPos = durChapterPos + book.durChapterTitle = durChapterTitle + book.totalChapterNum = totalChapterNum + setBookData(book) + status.postValue(AppConst.complete) + }, error = { + status.postValue(AppConst.error) + }, showErrorToast = false + ) + + } + + private fun setBookData(book: Book) { + bookData.postValue(book) + if (inBookshelf) { + App.db.getBookDao().update(book) + } + } + + fun getRecommend(intent: Intent) { + launch({ + bookTypeId = intent.getIntExtra(IntentAction.bookTypeId,0) + val data = bookRepository.getSimilarRecommend(bookTypeId) + mList = data.recommendBookList + bookListResp.value = mList + }, { + }, showErrorToast = false) + } + + fun delBook(success: (() -> Unit)? = null) { + execute { + bookData.value?.let { + App.db.getBookDao().delete(it) + inBookshelf = false + } + }.onSuccess { + success?.invoke() + } + } + + fun addToBookshelf(success: (() -> Unit)?) { + execute { + bookData.value?.let { book -> + book.bookTypeId = bookTypeId + App.db.getBookDao().getBook(book.bookId.toString())?.let { + book.durChapterPos = it.durChapterPos + book.durChapterTitle = it.durChapterTitle + } + App.db.getBookDao().insert(book) + } + + inBookshelf = true + }.onSuccess { + success?.invoke() + } + } + + fun saveBook(success: (() -> Unit)? = null) { + execute { + bookData.value?.let { book -> + App.db.getBookDao().getBook(book.bookId.toString())?.let { + book.durChapterPos = it.durChapterPos + book.durChapterTitle = it.durChapterTitle + } + App.db.getBookDao().insert(book) + if (ReadBook.book?.bookName == book.bookName && ReadBook.book?.authorPenname == book.authorPenname) { + ReadBook.book = book + } + } + }.onSuccess { + success?.invoke() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/BaseBookAdapter.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/BaseBookAdapter.kt new file mode 100644 index 0000000..53d5374 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/BaseBookAdapter.kt @@ -0,0 +1,26 @@ +package com.novel.read.ui.main.bookshelf + +import androidx.core.os.bundleOf +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.data.db.entity.Book + +abstract class BaseBookAdapter(layoutId: Int) : BaseQuickAdapter(layoutId){ + + + fun notification(bookId: Long) { + for (i in 0 until itemCount) { + getItem(i).let { + if (it.bookId == bookId) { + notifyItemChanged(i, bundleOf(Pair("refresh", null))) + return + } + } + } + } + + interface CallBack { + fun isUpdate(bookId: Long): Boolean + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/BookShelfAdapter.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/BookShelfAdapter.kt new file mode 100644 index 0000000..abf33a5 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/BookShelfAdapter.kt @@ -0,0 +1,74 @@ +package com.novel.read.ui.main.bookshelf + +import android.os.Bundle +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.constant.BookType +import com.novel.read.data.db.entity.Book +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.item_bookshelf_list.view.* +import kotlinx.android.synthetic.main.item_bookshelf_list.view.bv_unread +import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover +import kotlinx.android.synthetic.main.item_bookshelf_list.view.rl_loading +import kotlinx.android.synthetic.main.item_bookshelf_list.view.tv_name + +class BookShelfAdapter(private val callBack: CallBack) : BaseBookAdapter(R.layout.item_bookshelf_list) { + + + override fun convert(holder: BaseViewHolder, item: Book, payloads: List) { + super.convert(holder, item, payloads) + val bundle = payloads.getOrNull(0) as? Bundle + holder.itemView.run { + ATH.applyBackgroundTint(this) + if (bundle == null) { + tv_name.text = item.bookName + tv_author.text = item.authorPenname + tv_read.text = item.durChapterTitle + tv_last.text = item.keyWord + iv_cover.load(item.coverImageUrl, item.bookName, item.authorPenname) + upRefresh(holder, item) + } else { + tv_read.text = item.durChapterTitle + tv_last.text = item.keyWord + bundle.keySet().forEach { + when (it) { + "name" -> tv_name.text = item.bookName + "author" -> tv_author.text = item.authorPenname + "cover" -> iv_cover.load( + item.coverImageUrl, + item.bookName, + item.authorPenname + ) + "refresh" -> upRefresh(holder, item) + } + } + } + } + } + + override fun convert(holder: BaseViewHolder, item: Book) { + holder.itemView.run { + ATH.applyBackgroundTint(this) + tv_name.text = item.bookName + tv_author.text = item.authorPenname + tv_read.text = item.durChapterTitle + tv_last.text = item.keyWord + iv_cover.load(item.coverImageUrl, item.bookName, item.authorPenname) + upRefresh(holder, item) + } + } + + + private fun upRefresh(holder: BaseViewHolder, item: Book) { + if (item.origin != BookType.local && callBack.isUpdate(item.bookId)) { + holder.itemView.bv_unread.invisible() + holder.itemView.rl_loading.show() + } else { + holder.itemView.rl_loading.hide() + holder.itemView.bv_unread.setBadgeCount(0) + holder.itemView.bv_unread.setHighlight(true) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/BookViewModel.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/BookViewModel.kt new file mode 100644 index 0000000..d7ffdb5 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/BookViewModel.kt @@ -0,0 +1,11 @@ +package com.novel.read.ui.main.bookshelf + +import android.app.Application + +import com.novel.read.base.BaseViewModel + + +class BookViewModel(application: Application) : BaseViewModel(application) { + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/BooksAdapterGrid.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/BooksAdapterGrid.kt new file mode 100644 index 0000000..995dd97 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/BooksAdapterGrid.kt @@ -0,0 +1,61 @@ +package com.novel.read.ui.main.bookshelf + +import android.os.Bundle +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.constant.BookType +import com.novel.read.data.db.entity.Book +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.invisible +import kotlinx.android.synthetic.main.item_bookshelf_grid.view.* + +class BooksAdapterGrid(private val callBack: CallBack) : + BaseBookAdapter(R.layout.item_bookshelf_grid) { + + override fun convert(holder: BaseViewHolder, item: Book, payloads: List) { + super.convert(holder, item, payloads) + val bundle = payloads.getOrNull(0) as? Bundle + + holder.itemView.run { + ATH.applyBackgroundTint(this) + if (bundle == null) { + tv_name.text = item.bookName + iv_cover.load(item.coverImageUrl, item.bookName, item.authorPenname) + upRefresh(holder, item) + } else { + bundle.keySet().forEach { + when (it) { + "name" -> tv_name.text = item.bookName + "cover" -> iv_cover.load( + item.coverImageUrl, + item.bookName, + item.authorPenname + ) + "refresh" -> upRefresh(holder, item) + } + } + } + + } + } + + override fun convert(holder: BaseViewHolder, item: Book) { + holder.itemView.run { + ATH.applyBackgroundTint(this) + tv_name.text = item.bookName + iv_cover.load(item.coverImageUrl, item.bookName, item.authorPenname) + upRefresh(holder, item) + } + } + + private fun upRefresh(holder: BaseViewHolder, item: Book) { + if (item.origin != BookType.local && callBack.isUpdate(item.bookId)) { + holder.itemView.bv_unread.invisible() + holder.itemView.rl_loading.show() + } else { + holder.itemView.rl_loading.hide() + holder.itemView.bv_unread.setBadgeCount(0) + holder.itemView.bv_unread.setHighlight(true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/BookshelfFragment.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/BookshelfFragment.kt new file mode 100644 index 0000000..72e9a0d --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/BookshelfFragment.kt @@ -0,0 +1,192 @@ +package com.novel.read.ui.main.bookshelf + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.MutableLiveData +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.VMBaseFragment +import com.novel.read.constant.EventBus +import com.novel.read.constant.IntentAction +import com.novel.read.constant.PreferKey +import com.novel.read.data.db.entity.Book +import com.novel.read.help.AppConfig +import com.novel.read.help.IntentDataHelp +import com.novel.read.lib.ATH +import com.novel.read.lib.dialogs.alert +import com.novel.read.lib.dialogs.noButton +import com.novel.read.lib.dialogs.okButton +import com.novel.read.ui.MainViewModel +import com.novel.read.ui.info.BookInfoActivity +import com.novel.read.ui.main.bookshelf.arrange.ArrangeBookActivity +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.ui.search.SearchActivity +import com.novel.read.utils.BooksDiffCallBack +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.* +import kotlinx.android.synthetic.main.fragment_book_shelf.* +import kotlinx.android.synthetic.main.view_title_bar.* +import org.jetbrains.anko.startActivity + +class BookshelfFragment : VMBaseFragment(R.layout.fragment_book_shelf), BaseBookAdapter.CallBack { + + + override val viewModel: BookViewModel by viewModels() + private val activityViewModel: MainViewModel + by activityViewModels() + private lateinit var booksAdapter: BaseBookAdapter + private var bookshelfLiveData = MutableLiveData>() + private lateinit var selectBook: Book + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(toolbar) + initRecycleView() + upRecyclerData() + } + + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_bookshelf, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + when (item.itemId) { + R.id.menu_search -> startActivity() + R.id.menu_update_toc -> { +// val group = bookGroups[tab_layout.selectedTabPosition] +// val fragment = fragmentMap[group.groupId] +// fragment?.getBooks()?.let { +// activityViewModel.upToc(it) +// } + } + R.id.menu_bookshelf_layout -> configBookshelf() + R.id.menu_arrange_bookshelf -> startActivity() + } + } + + private fun initRecycleView() { + ATH.applyEdgeEffectColor(rlv_book_shelf) + + refresh_layout.setColorSchemeColors(accentColor) + refresh_layout.setOnRefreshListener { + refresh_layout.isRefreshing = false + activityViewModel.upToc(booksAdapter.data) + } + + val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) + if (bookshelfLayout == 0) { + rlv_book_shelf.layoutManager = LinearLayoutManager(context) + booksAdapter = BookShelfAdapter(this) + } else { + rlv_book_shelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2) + booksAdapter = BooksAdapterGrid(this) + } + rlv_book_shelf.adapter = booksAdapter + booksAdapter.setEmptyView(R.layout.view_empty) + booksAdapter.setDiffCallback(BooksDiffCallBack()) + + booksAdapter.setOnItemClickListener { adapter, view, position -> + selectBook = adapter.data[position] as Book + activity?.startActivity( + Pair(IntentAction.bookId, selectBook.bookId), + Pair("key", IntentDataHelp.putData(selectBook)) + ) +// ProcessLifecycleOwner.get().lifecycle.addObserver(App.ApplicationObserverInRead()) + } + + booksAdapter.setOnItemLongClickListener { adapter, view, position -> + selectBook = adapter.data[position] as Book + activity?.let { BookInfoActivity.actionBookInfo(it, selectBook.bookId, selectBook.bookTypeId) } + true + } + } + + + private fun upRecyclerData() { + bookshelfLiveData.removeObservers(this) + bookshelfLiveData.value = App.db.getBookDao().getAllBooks() + + bookshelfLiveData.observe(viewLifecycleOwner, { list -> + Log.e("BookFragment", "observeLiveBus: 开始更新") + booksAdapter.isUseEmpty = list.isEmpty() + val books = when (getPrefInt(PreferKey.bookshelfSort)) { + 1 -> list.sortedByDescending { it.lastUpdateChapterDate } + 2 -> list.sortedBy { it.bookName } + else -> list.sortedByDescending { it.durChapterTime } + } + + booksAdapter.setList(books.toMutableList()) + }) + + } + + fun gotoTop() { + if (AppConfig.isEInkMode) { + rlv_book_shelf.scrollToPosition(0) + } else { + rlv_book_shelf.smoothScrollToPosition(0) + } + } + + @SuppressLint("InflateParams") + private fun configBookshelf() { + requireContext().alert(titleResource = R.string.bookshelf_layout) { + val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) + val bookshelfSort = getPrefInt(PreferKey.bookshelfSort) + val root = LayoutInflater.from(requireContext()) + .inflate(R.layout.dialog_bookshelf_config, null).apply { + rg_layout.checkByIndex(bookshelfLayout) + rg_sort.checkByIndex(bookshelfSort) + } + customView = root + okButton { + root.apply { + var changed = false + if (bookshelfLayout != rg_layout.getCheckedIndex()) { + putPrefInt(PreferKey.bookshelfLayout, rg_layout.getCheckedIndex()) + changed = true + } + if (bookshelfSort != rg_sort.getCheckedIndex()) { + putPrefInt(PreferKey.bookshelfSort, rg_sort.getCheckedIndex()) + changed = true + } + if (changed) { + activity?.recreate() + } + } + } + noButton() + }.show().applyTint() + } + + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.UP_BOOK) { + bookshelfLiveData.value = App.db.getBookDao().getAllBooks() + } + observeEvent(EventBus.UPDATE_BOOK) { + booksAdapter.notification(it) + } + observeEvent(EventBus.SHOW_AD) { + activity?.startActivity( + Pair(IntentAction.bookId, selectBook.bookId), + Pair("key", IntentDataHelp.putData(selectBook)) + ) + } + + } + + override fun isUpdate(bookId: Long): Boolean { + return bookId in activityViewModel.updateList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookActivity.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookActivity.kt new file mode 100644 index 0000000..a748514 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookActivity.kt @@ -0,0 +1,71 @@ +package com.novel.read.ui.main.bookshelf.arrange + +import android.os.Bundle +import android.util.Log +import androidx.recyclerview.widget.LinearLayoutManager +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.PreferKey +import com.novel.read.data.db.entity.Book +import com.novel.read.lib.ATH +import com.novel.read.lib.dialogs.alert +import com.novel.read.lib.dialogs.okButton +import com.novel.read.ui.widget.VerticalDivider +import com.novel.read.utils.ext.applyTint +import com.novel.read.utils.ext.getPrefInt +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_arrange_book.* + +class ArrangeBookActivity : VMBaseActivity(R.layout.activity_arrange_book),ArrangeBookAdapter.CallBack { + + private lateinit var adapter: ArrangeBookAdapter + + override val viewModel: ArrangeBookViewModel + get() = getViewModel(ArrangeBookViewModel::class.java) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initBookData() + } + + private fun initView() { + ATH.applyEdgeEffectColor(recycler_view) + recycler_view.layoutManager = LinearLayoutManager(this) + recycler_view.addItemDecoration(VerticalDivider(this)) + adapter = ArrangeBookAdapter(this) + recycler_view.adapter = adapter + } + + private fun initBookData() { + viewModel.booksLiveData.removeObservers(this) + viewModel.booksLiveData.value = App.db.getBookDao().getAllBooks() + + viewModel.booksLiveData.observe(this, { list -> + Log.e("ArrangeBookActivity", "observeLiveBus: 开始更新") + adapter.isUseEmpty = list.isEmpty() + val books = when (getPrefInt(PreferKey.bookshelfSort)) { + 1 -> list.sortedByDescending { it.lastUpdateChapterDate } + 2 -> list.sortedBy { it.bookName } + else -> list.sortedByDescending { it.durChapterTime } + } + + adapter.setList(books.toMutableList()) + }) + } + + override fun upSelectCount() { + } + + override fun updateBook(vararg book: Book) { + } + + override fun deleteBook(book: Book) { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { + viewModel.deleteBook(book) + } + }.show().applyTint() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookAdapter.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookAdapter.kt new file mode 100644 index 0000000..1bc810e --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookAdapter.kt @@ -0,0 +1,36 @@ +package com.novel.read.ui.main.bookshelf.arrange + +import android.view.View +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.db.entity.Book +import com.novel.read.utils.ext.backgroundColor +import kotlinx.android.synthetic.main.item_arrange_book.view.* +import org.jetbrains.anko.backgroundColor +import org.jetbrains.anko.sdk27.listeners.onClick + +class ArrangeBookAdapter(val callBack: CallBack) : BaseQuickAdapter(R.layout.item_arrange_book) { + override fun convert(holder: BaseViewHolder, item: Book) { + holder.itemView.run { + backgroundColor = context.backgroundColor + tv_name.text = item.bookName + tv_author.text = item.authorPenname + tv_author.visibility = if (item.authorPenname.isEmpty()) View.GONE else View.VISIBLE +// checkbox.isChecked = selectedBooks.contains(item) + iv_book.load(item.coverImageUrl, item.bookName, item.authorPenname) + tv_delete.onClick { + getItem(holder.layoutPosition)?.let { + callBack.deleteBook(it) + } + } + } + } + + + interface CallBack { + fun upSelectCount() + fun updateBook(vararg book: Book) + fun deleteBook(book: Book) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookViewModel.kt b/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookViewModel.kt new file mode 100644 index 0000000..d161456 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/bookshelf/arrange/ArrangeBookViewModel.kt @@ -0,0 +1,20 @@ +package com.novel.read.ui.main.bookshelf.arrange + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import com.novel.read.App +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.EventBus +import com.novel.read.data.db.entity.Book +import com.novel.read.utils.ext.postEvent + +class ArrangeBookViewModel(application: Application) : BaseViewModel(application) { + var booksLiveData = MutableLiveData>() + + fun deleteBook(book: Book) { + App.db.getBookDao().delete(book) + booksLiveData.value = App.db.getBookDao().getAllBooks() + postEvent(EventBus.UP_BOOK, 0L) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/MailFragment.kt b/app/src/main/java/com/novel/read/ui/main/mail/MailFragment.kt new file mode 100644 index 0000000..f7577b1 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/MailFragment.kt @@ -0,0 +1,98 @@ +package com.novel.read.ui.main.mail + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.novel.read.R +import com.novel.read.base.VMBaseFragment +import com.novel.read.constant.AppConst +import com.novel.read.data.model.* +import com.novel.read.lib.ATH +import com.novel.read.ui.main.mail.pick.PickAdapter +import com.novel.read.ui.search.SearchActivity +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.fragment_mail.rlv_home +import kotlinx.android.synthetic.main.view_title_bar.* +import java.util.ArrayList + +class MailFragment : VMBaseFragment(R.layout.fragment_mail) { + + override val viewModel: MailViewModel + get() = getViewModel(MailViewModel::class.java) + private lateinit var adapter: PickAdapter + private val data: MutableList = ArrayList() + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(toolbar) + ATH.applyEdgeEffectColor(rlv_home) + initRecycleView() + upRecyclerData() + } + + private fun initRecycleView() { + rlv_home.layoutManager = LinearLayoutManager(context) + adapter = PickAdapter(data) + rlv_home.adapter = adapter + } + + private fun upRecyclerData() { + onRefresh() + viewModel.run { + homeResp.observe(viewLifecycleOwner) { + data.clear() + val hotEntity = HotEntity(it.starRank) + val clickEntity = ClickEntity(it.wordNumRank) + val recommendEntity = RecommendEntity(it.recommendRank) + val endEntity = EndEntity(it.clickRank) + data.add(TypeEntity()) + data.add(hotEntity) + data.add(clickEntity) + data.add(recommendEntity) + data.add(endEntity) + adapter.setList(data) + } + + refreshStatus.observe(viewLifecycleOwner) { + when (it) { + AppConst.loading -> { + adapter.isUseEmpty = true + } + AppConst.complete -> { + adapter.isUseEmpty = false + } + else -> { + adapter.setEmptyView(getErrorView()) + adapter.isUseEmpty = true + } + } + } + + } + } + + private fun onRefresh() { + adapter.setEmptyView(R.layout.view_loading) + viewModel.getAll() + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_home, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_mail, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + when (item.itemId) { + R.id.menu_search -> startActivity() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/MailViewModel.kt b/app/src/main/java/com/novel/read/ui/main/mail/MailViewModel.kt new file mode 100644 index 0000000..4faeb60 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/MailViewModel.kt @@ -0,0 +1,23 @@ +package com.novel.read.ui.main.mail + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.data.model.HomeResp +import com.novel.read.network.repository.HomeRepository + +class MailViewModel(application: Application) : BaseViewModel(application) { + private val homeRepository by lazy { HomeRepository() } + var homeResp = MutableLiveData() + val refreshStatus = MutableLiveData() + fun getAll() { + refreshStatus.value = AppConst.loading + launch(block = { + homeResp.value = homeRepository.getHomeBook(AppConst.home) + refreshStatus.value = AppConst.complete + }, error = { + refreshStatus.value = AppConst.error + },showErrorToast = false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/RecommentFragment.kt b/app/src/main/java/com/novel/read/ui/main/mail/RecommentFragment.kt new file mode 100644 index 0000000..239d05f --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/RecommentFragment.kt @@ -0,0 +1,4 @@ +package com.novel.read.ui.main.mail + +class RecommentFragment { +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/pick/ClickAdapter.kt b/app/src/main/java/com/novel/read/ui/main/mail/pick/ClickAdapter.kt new file mode 100644 index 0000000..d9a0375 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/pick/ClickAdapter.kt @@ -0,0 +1,24 @@ +package com.novel.read.ui.main.mail.pick + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.ClickRank +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_click_item.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class ClickAdapter :BaseQuickAdapter(R.layout.item_click_item) { + + override fun convert(holder: BaseViewHolder, item: ClickRank) { + holder.itemView.run { + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_name.text = item.getBBookName() + tv_count.text = context.getString(R.string.book_word, item.wordCount / 10000) + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/pick/EndAdapter.kt b/app/src/main/java/com/novel/read/ui/main/mail/pick/EndAdapter.kt new file mode 100644 index 0000000..f17a152 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/pick/EndAdapter.kt @@ -0,0 +1,25 @@ +package com.novel.read.ui.main.mail.pick + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.EndRank +import com.novel.read.lib.ATH +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_end_item.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class EndAdapter :BaseQuickAdapter(R.layout.item_end_item) { + + override fun convert(holder: BaseViewHolder, item: EndRank) { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_name.text = item.getBBookName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/pick/HotAdapter.kt b/app/src/main/java/com/novel/read/ui/main/mail/pick/HotAdapter.kt new file mode 100644 index 0000000..0f7a2b4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/pick/HotAdapter.kt @@ -0,0 +1,24 @@ +package com.novel.read.ui.main.mail.pick + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.HotRank +import com.novel.read.lib.ATH +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_hot_item.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class HotAdapter :BaseQuickAdapter(R.layout.item_hot_item) { + + override fun convert(holder: BaseViewHolder, item: HotRank) { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_cover.load(item.coverImageUrl, item.getBName(), item.getBAuthor()) + tv_name.text = item.getBName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/mail/pick/PickAdapter.kt b/app/src/main/java/com/novel/read/ui/main/mail/pick/PickAdapter.kt new file mode 100644 index 0000000..7362aef --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/pick/PickAdapter.kt @@ -0,0 +1,150 @@ +package com.novel.read.ui.main.mail.pick + +import androidx.recyclerview.widget.GridLayoutManager +import com.chad.library.adapter.base.BaseMultiItemQuickAdapter +import com.chad.library.adapter.base.entity.MultiItemEntity +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.constant.AppConst +import com.novel.read.constant.IntentAction +import com.novel.read.constant.LayoutType +import com.novel.read.data.model.* +import com.novel.read.lib.ATH +import com.novel.read.ui.channel.ChannelActivity +import com.novel.read.ui.daily.DailyActivity +import com.novel.read.ui.end.EndActivity +import com.novel.read.ui.rank.RankActivity +import kotlinx.android.synthetic.main.item_hot.view.* +import kotlinx.android.synthetic.main.item_pick_head.view.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.startActivity + + +class PickAdapter(data: MutableList?) : + BaseMultiItemQuickAdapter(data) { + + init { + addItemType(LayoutType.OTHER, R.layout.item_pick_head) + addItemType(LayoutType.HOT, R.layout.item_hot) + addItemType(LayoutType.CLICK, R.layout.item_hot) + addItemType(LayoutType.RECOMMEND, R.layout.item_hot) + addItemType(LayoutType.END, R.layout.item_hot) + } + + override fun convert(holder: BaseViewHolder, item: T) { + when (item) { + is TypeEntity -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + tv_daily.onClick { + context.startActivity() + } + tv_rank.onClick { + context.startActivity( + Pair(IntentAction.rankType, LayoutType.HOT), + Pair(IntentAction.homeType, AppConst.home) + ) + } + tv_end.onClick { + context.startActivity() + } + + tv_type.onClick { + context.startActivity() + } + + } + } + is HotEntity -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + rlv_hot.setHasFixedSize(true) + ll_title.onClick { + context.startActivity( + Pair(IntentAction.rankType, LayoutType.HOT), + Pair(IntentAction.homeType, AppConst.home) + ) + } + if (rlv_hot.layoutManager == null) { + rlv_hot.layoutManager = GridLayoutManager(context, 4) + } + if (rlv_hot.adapter == null) { + val hotAdapter = HotAdapter() + hotAdapter.setList(item.hotRanks) + rlv_hot.adapter = hotAdapter + } + } + } + is ClickEntity -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_head.setImageResource(R.drawable.ic_click) + tv_head.text=context.getString(R.string.click_title) + ll_title.onClick { + context.startActivity( + Pair(IntentAction.rankType, LayoutType.CLICK), + Pair(IntentAction.homeType, AppConst.home) + ) + } + rlv_hot.setHasFixedSize(true) + if (rlv_hot.layoutManager == null) { + rlv_hot.layoutManager = GridLayoutManager(context, 2) + } + if (rlv_hot.adapter == null) { + val clickAdapter = ClickAdapter() + clickAdapter.setList(item.clickRanks) + rlv_hot.adapter = clickAdapter + } + } + } + is RecommendEntity -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_head.setImageResource(R.drawable.ic_recommend) + tv_head.text=context.getString(R.string.recommend_title) + ll_title.onClick { + context.startActivity( + Pair(IntentAction.rankType, LayoutType.RECOMMEND), + Pair(IntentAction.homeType, AppConst.home) + ) + } + rlv_hot.setHasFixedSize(true) + if (rlv_hot.layoutManager == null) { + rlv_hot.layoutManager = GridLayoutManager(context, 4) + } + if (rlv_hot.adapter == null) { + val recommendAdapter = RecommendAdapter() + recommendAdapter.setList(item.recommendRanks) + rlv_hot.adapter = recommendAdapter + } + } + } + is EndEntity -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_head.setImageResource(R.drawable.ic_end) + tv_head.text=context.getString(R.string.end_title) + ll_title.onClick { + context.startActivity( + Pair(IntentAction.rankType, LayoutType.END), + Pair(IntentAction.homeType, AppConst.home) + ) + } + rlv_hot.setHasFixedSize(true) + if (rlv_hot.layoutManager == null) { + rlv_hot.layoutManager = GridLayoutManager(context, 4) + } + if (rlv_hot.adapter == null) { + val endAdapter = EndAdapter() + endAdapter.setList(item.endRanks) + rlv_hot.adapter = endAdapter + } + } + } + } + } + +} + + + diff --git a/app/src/main/java/com/novel/read/ui/main/mail/pick/RecommendAdapter.kt b/app/src/main/java/com/novel/read/ui/main/mail/pick/RecommendAdapter.kt new file mode 100644 index 0000000..baef5fd --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/mail/pick/RecommendAdapter.kt @@ -0,0 +1,42 @@ +package com.novel.read.ui.main.mail.pick + +import com.chad.library.adapter.base.BaseMultiItemQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.constant.LayoutType +import com.novel.read.data.model.RecommendRank +import com.novel.read.lib.ATH +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_end_item.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class RecommendAdapter : BaseMultiItemQuickAdapter() { + + init { + addItemType(LayoutType.RECOMMEND, R.layout.item_recomment_item) + addItemType(LayoutType.HEAD, R.layout.item_recomment_first_item) + } + + override fun convert(holder: BaseViewHolder, item: RecommendRank) { + when (item.itemType) { + LayoutType.HEAD -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_name.text = item.getBBookName() + } + } + LayoutType.RECOMMEND -> { + holder.itemView.run { + ATH.applyBackgroundTint(this) + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_name.text = item.getBBookName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/my/MyFragment.kt b/app/src/main/java/com/novel/read/ui/main/my/MyFragment.kt new file mode 100644 index 0000000..996a1da --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/my/MyFragment.kt @@ -0,0 +1,60 @@ +package com.novel.read.ui.main.my + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseFragment +import com.novel.read.ui.feedback.FeedBackActivity +import com.novel.read.ui.setting.SettingActivity +import com.novel.read.ui.widget.dialog.AppraiseDialog +import com.novel.read.utils.ext.startActivity +import com.novel.read.utils.ext.toast +import kotlinx.android.synthetic.main.fragment_my.* +import kotlinx.android.synthetic.main.view_title_bar.* + +class MyFragment : BaseFragment(R.layout.fragment_my) { + + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(toolbar) + tv_options.setOnClickListener { + startActivity() + } + tv_setting.setOnClickListener { + startActivity() + } + tv_appraise.setOnClickListener { + val dialog = AppraiseDialog(requireActivity()) + dialog.appraiseDialog { + goToMarket(App.INSTANCE, App.INSTANCE.packageName) + dialog.dismiss() + } + dialog.show() + } + } + + @SuppressLint("QueryPermissionsNeeded") + fun goToMarket(context: Context, packageName: String) { + val uri = Uri.parse("market://details?id=$packageName") + val goToMarket = Intent(Intent.ACTION_VIEW, uri) + val googlePlay = "com.android.vending" //这里对应的是谷歌商店,跳转别的商店改成对应的即可 + goToMarket.setPackage(googlePlay) //这里对应的是谷歌商店,跳转别的商店改成对应的即可 + try { + startActivity(goToMarket) + } catch (e: ActivityNotFoundException) { + if (goToMarket.resolveActivity(context.packageManager) != null) { //有浏览器 + startActivity(goToMarket) + } else { + toast(R.string.no_google) + } + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/main/my/ToolsAdapter.kt b/app/src/main/java/com/novel/read/ui/main/my/ToolsAdapter.kt new file mode 100644 index 0000000..bf37719 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/main/my/ToolsAdapter.kt @@ -0,0 +1,19 @@ +package com.novel.read.ui.main.my + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.Tools +import com.novel.read.lib.ATH +import kotlinx.android.synthetic.main.item_tools.view.* + +class ToolsAdapter : BaseQuickAdapter(R.layout.item_tools) { + + override fun convert(holder: BaseViewHolder, item: Tools) { + holder.itemView.run { + ATH.applyBackgroundTint(this) + tv_tools.text = item.title + iv_tools.setImageResource(item.img) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/rank/RankActivity.kt b/app/src/main/java/com/novel/read/ui/rank/RankActivity.kt new file mode 100644 index 0000000..86d830a --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/rank/RankActivity.kt @@ -0,0 +1,133 @@ +package com.novel.read.ui.rank + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.listener.OnLoadMoreListener +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.constant.IntentAction +import com.novel.read.constant.LayoutType +import com.novel.read.data.model.* +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.getViewModel +import kotlinx.android.synthetic.main.activity_rank.* + +class RankActivity : VMBaseActivity(R.layout.activity_rank) { + + override val viewModel: RankViewModel + get() = getViewModel(RankViewModel::class.java) + + private lateinit var adapter: RankAdapter + private lateinit var typeAdapter: TypeAdapter + private lateinit var mTypes: List + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(rlv_rank) + ATH.applyEdgeEffectColor(lv_type) + initRecyclerView() + upRecyclerData() + initLoadMore() + } + + private fun initRecyclerView() { + rlv_rank.layoutManager = LinearLayoutManager(this) + adapter = RankAdapter() + rlv_rank.adapter = adapter + lv_type.layoutManager = LinearLayoutManager(this) + typeAdapter = TypeAdapter() + lv_type.adapter = typeAdapter + + } + + private fun upRecyclerData() { + viewModel.pageType = intent.getIntExtra(IntentAction.homeType, AppConst.home) + viewModel.rankType = intent.getIntExtra(IntentAction.rankType, LayoutType.HOT) + + when (viewModel.pageType) { + AppConst.home -> { + mTypes = rankList(viewModel.rankType) + } + AppConst.man -> { + mTypes = manList(viewModel.rankType) + } + AppConst.woman -> { + mTypes = womanList(viewModel.rankType) + } + } + + typeAdapter.setList(mTypes) + typeAdapter.setOnItemClickListener { adapter, view, position -> + for (i in 0 until typeAdapter.data.size) { + typeAdapter.data[i].check = false + } + typeAdapter.data[position].check = true + typeAdapter.notifyDataSetChanged() + viewModel.rankType = typeAdapter.data[position].rankType + viewModel.page = 1 + onRefresh() + } + + onRefresh() + viewModel.run { + bookListResp.observe(this@RankActivity) { + adapter.setList(it) + } + pageStatus.observe(this@RankActivity) { + when (it) { + AppConst.loading -> { + adapter.isUseEmpty = true + } + AppConst.complete -> { + adapter.isUseEmpty = false + } + AppConst.loadMore -> { + adapter.loadMoreModule.isEnableLoadMore = true + } + AppConst.loadComplete -> { + adapter.loadMoreModule.loadMoreComplete() + } + AppConst.noMore -> { + adapter.loadMoreModule.loadMoreEnd() + } + AppConst.loadMoreFail -> { + adapter.loadMoreModule.loadMoreFail() + } + else -> { + adapter.setList(null) + adapter.setEmptyView(getErrorView()) + adapter.isUseEmpty = true + } + } + } + } + } + + private fun initLoadMore() { + adapter.loadMoreModule.setOnLoadMoreListener(object : OnLoadMoreListener { + override fun onLoadMore() { + if (viewModel.pageStatus.value == AppConst.loadMore) { + return + } + viewModel.loadMore() + } + }) + adapter.loadMoreModule.isAutoLoadMore = true + adapter.loadMoreModule.isEnableLoadMoreIfNotFullPage = false + } + + private fun onRefresh() { + adapter.setList(null) + adapter.setEmptyView(R.layout.view_loading) + viewModel.initData() + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_rank, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/rank/RankAdapter.kt b/app/src/main/java/com/novel/read/ui/rank/RankAdapter.kt new file mode 100644 index 0000000..dabba57 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/rank/RankAdapter.kt @@ -0,0 +1,28 @@ +package com.novel.read.ui.rank + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.BookListResp +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_book_common.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class RankAdapter : + BaseQuickAdapter(R.layout.item_book_common), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: BookListResp) { + holder.itemView.run { + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_book_name.text = item.getBBookName() + tv_book_description.text = item.getBIntroduction() + tv_book_author.text = item.getBAuthorName() + tv_word.text = context.getString(R.string.book_word, item.wordCount / 10000) + tv_category.text = item.getBCategoryName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/rank/RankViewModel.kt b/app/src/main/java/com/novel/read/ui/rank/RankViewModel.kt new file mode 100644 index 0000000..ba347c3 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/rank/RankViewModel.kt @@ -0,0 +1,52 @@ +package com.novel.read.ui.rank + +import android.app.Application +import android.util.Log +import androidx.lifecycle.MutableLiveData +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.data.model.BookListResp +import com.novel.read.network.repository.BookRepository + +class RankViewModel(application: Application) : BaseViewModel(application) { + + private val bookRepository by lazy { BookRepository() } + var bookListResp = MutableLiveData>() + private lateinit var mList: MutableList + val pageStatus = MutableLiveData() + var page: Int = 1 + private var pageSize: Int = 20 + var rankType: Int = 1 + var pageType: Int = 1 + + fun initData() { + launch(block = { + pageStatus.value = AppConst.loading + Log.e("initData", "initData: $rankType", ) + val data = bookRepository.getBooRank(page, pageSize, rankType) + mList = data.bookRankList + bookListResp.value = mList + pageStatus.value = AppConst.complete + },error = { + pageStatus.value = AppConst.error + }) + } + + fun loadMore() { + Log.e("loadMore", "loadMore: $page") + launch(block = { + pageStatus.value = AppConst.loadMore + page++ + val mData = bookRepository.getBooRank(page, pageSize, rankType).bookRankList + mList.addAll(mData) + bookListResp.value = mList + pageStatus.value = AppConst.loadComplete + if (pageSize > mData.size) { + pageStatus.value = AppConst.noMore + } + }, error = { + page-- + pageStatus.value = AppConst.loadMoreFail + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/rank/TypeAdapter.kt b/app/src/main/java/com/novel/read/ui/rank/TypeAdapter.kt new file mode 100644 index 0000000..301c608 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/rank/TypeAdapter.kt @@ -0,0 +1,26 @@ +package com.novel.read.ui.rank + +import android.view.View +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.TypeName +import kotlinx.android.synthetic.main.item_type.view.* + +class TypeAdapter : + BaseQuickAdapter(R.layout.item_type), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: TypeName) { + holder.itemView.run { + tv_type.text = item.name + if (item.check) { + iv_check.visibility = View.VISIBLE + ll_bg.setBackgroundColor(context.resources.getColor(R.color.background)) + } else { + iv_check.visibility = View.GONE + ll_bg.setBackgroundColor(context.resources.getColor(R.color.background_menu)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/ReadBookActivity.kt b/app/src/main/java/com/novel/read/ui/read/ReadBookActivity.kt new file mode 100644 index 0000000..5cf851d --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/ReadBookActivity.kt @@ -0,0 +1,690 @@ +package com.novel.read.ui.read + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.* +import androidx.core.view.isVisible +import androidx.lifecycle.observe +import com.novel.read.BuildConfig +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.EventBus +import com.novel.read.constant.PreferKey +import com.novel.read.constant.Status +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.help.ReadBookConfig +import com.novel.read.lib.ATH +import com.novel.read.lib.dialogs.alert +import com.novel.read.lib.dialogs.noButton +import com.novel.read.lib.dialogs.okButton +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.help.ReadAloud +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.chapter.ChapterListActivity +import com.novel.read.ui.info.BookInfoActivity +import com.novel.read.ui.read.config.ReadAdjustDialog +import com.novel.read.ui.read.config.ReadAloudDialog +import com.novel.read.ui.read.config.ReadStyleDialog +import com.novel.read.ui.read.page.ContentTextView +import com.novel.read.ui.read.page.PageView +import com.novel.read.ui.read.page.TextPageFactory +import com.novel.read.ui.read.page.delegate.PageDelegate +import com.novel.read.ui.widget.dialog.AutoReadDialog +import com.novel.read.ui.widget.dialog.TextDialog +import com.novel.read.user.VipHelper +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.activity_read_book.* +import kotlinx.android.synthetic.main.view_read_menu.* +import kotlinx.coroutines.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.startActivityForResult +import java.lang.Runnable + +class ReadBookActivity : VMBaseActivity(R.layout.activity_read_book), + View.OnTouchListener, PageView.CallBack, TextActionMenu.CallBack, ContentTextView.CallBack, + ReadBook.CallBack, + ReadMenu.CallBack, + ReadAloudDialog.CallBack { + + override val viewModel: ReadBookViewModel + get() = getViewModel(ReadBookViewModel::class.java) + + override val selectedText: String + get() = page_view.curPage.selectedText + + private val requestCodeChapterList = 568 + private val requestCodeSearchResult = 123 + private var menu: Menu? = null + private var textActionMenu: TextActionMenu? = null + + override val scope: CoroutineScope get() = this + override val isInitFinish: Boolean get() = viewModel.isInitFinish + override val isScroll: Boolean get() = page_view.isScroll + private val mHandler = Handler(Looper.getMainLooper()) + private val keepScreenRunnable: Runnable = + Runnable { ReadBookActivityHelp.keepScreenOn(window, false) } + override var autoPageProgress = 0 + + override var isAutoPage = false + private var screenTimeOut: Long = 0 + private var loadStates: Boolean = false + override val pageFactory: TextPageFactory get() = page_view.pageFactory + override val headerHeight: Int get() = page_view.curPage.headerHeight + + override fun onCreate(savedInstanceState: Bundle?) { + ReadBook.msg = null + ReadBookActivityHelp.setOrientation(this) + super.onCreate(savedInstanceState) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ReadBookActivityHelp.upLayoutInDisplayCutoutMode(window) + initView() + upScreenTimeOut() + setScreenBrightness(getPrefInt("brightness", 100)) + ReadBook.callBack = this + ReadBook.titleDate.observe(this) { + title_bar.title = it + upMenu() + upView() + } + viewModel.initData(intent) + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + upSystemUiVisibility() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + page_view.upStatusBar() + ReadBook.loadContent(resetPageOffset = false) + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.read_book, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + private fun upMenu() { + menu?.let { menu -> + ReadBook.book?.let { book -> + + } + } + } + + /** + * 菜单 + */ + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_refresh -> { + ReadBook.book?.let { + ReadBook.curTextChapter = null + page_view.upContent() + viewModel.refreshContent(it) + } + } + R.id.menu_download -> ReadBookActivityHelp.showDownloadDialog(this) + R.id.menu_add_bookmark -> ReadBookActivityHelp.showBookMark(this) + R.id.menu_copy_text -> + TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) + R.id.menu_update_toc -> ReadBook.book?.let { + loadChapterList(it) + } + R.id.menu_page_anim -> ReadBookActivityHelp.showPageAnimConfig(this) { + page_view.upPageAnim() + } + R.id.menu_book_info -> ReadBook.book?.let { + BookInfoActivity.actionBookInfo(this, it.bookId, it.bookTypeId) + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onResume() { + super.onResume() + ReadBook.readStartTime = System.currentTimeMillis() + upSystemUiVisibility() + page_view.upTime() + } + + override fun onPause() { + super.onPause() + ReadBook.saveRead() + upSystemUiVisibility() + } + + override fun upNavigationBarColor() { + when { + read_menu == null -> return + read_menu.isVisible -> { + ATH.setNavigationBarColorAuto(this) + } + ReadBookConfig.bg is ColorDrawable -> { + ATH.setNavigationBarColorAuto(this, ReadBookConfig.bgMeanColor) + } + else -> { + ATH.setNavigationBarColorAuto(this, Color.BLACK) + } + } + } + + + /** + * 初始化View + */ + private fun initView() { + cursor_left.setColorFilter(accentColor) + cursor_right.setColorFilter(accentColor) + cursor_left.setOnTouchListener(this) + cursor_right.setOnTouchListener(this) + tv_chapter_name.onClick { + + } + tv_chapter_url.onClick { + runCatching { + val url = tv_chapter_url.text.toString() + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(url) + startActivity(intent) + } + } + + adView.setBackgroundColor(ReadBookConfig.bgMeanColor) + adView.alpha = 0.3f + if (VipHelper.showAd()) { +// adView.visibility = View.VISIBLE +// //底部banner google +// val mPublisherAdView = PublisherAdView(this) +// mPublisherAdView.adUnitId = "ca-app-pub-5528897088703176/7116750145" +// adView.removeAllViews() +// adView.addView(mPublisherAdView) +// val adRequest = PublisherAdRequest.Builder().build() +// mPublisherAdView.setAdSizes(getAdSize(), AdSize.BANNER) +// mPublisherAdView.loadAd(adRequest) + } else { + adView.visibility = View.GONE + } + + + } + +// private fun getAdSize(): AdSize? { +// val display = windowManager.defaultDisplay +// val outMetrics = DisplayMetrics() +// display.getMetrics(outMetrics) +// val widthPixels = outMetrics.widthPixels.toFloat() +// val density = outMetrics.density +// val adWidth = (widthPixels / density).toInt() +// return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(this, adWidth) +// } + + override fun clickCenter() { + when { + BaseReadAloudService.isRun -> { + showReadAloudDialog() + } + isAutoPage -> { + AutoReadDialog().show(supportFragmentManager, "autoRead") + } + else -> { + read_menu.runMenuIn() + } + } + } + + + override fun showTextActionMenu() { + textActionMenu ?: let { + textActionMenu = TextActionMenu(this, this) + } + textActionMenu?.let { popup -> + popup.contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val popupHeight = popup.contentView.measuredHeight + val x = text_menu_position.x.toInt() + var y = text_menu_position.y.toInt() - popupHeight + if (y < statusBarHeight) { + y = (cursor_left.y + cursor_left.height).toInt() + } + if (cursor_right.y > y && cursor_right.y < y + popupHeight) { + y = (cursor_right.y + cursor_right.height).toInt() + } + if (!popup.isShowing) { + popup.showAtLocation(text_menu_position, Gravity.TOP or Gravity.START, x, y) + } else { + popup.update( + x, y, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + } + } + + override fun loadChapterList(book: Book) { + ReadBook.upMsg(getString(R.string.toc_updateing)) + viewModel.loadChapterList(book) + } + + override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { + autoPageProgress = 0 + launch { + page_view.upContent(relativePosition, resetPageOffset) + seek_read_page.progress = ReadBook.durPageIndex + } + loadStates = false + } + + override fun upView() { + launch { + ReadBook.curTextChapter?.let { + tv_chapter_name.text = it.title +// tv_chapter_name.visible() + if (!ReadBook.isLocalBook) { + tv_chapter_url.text = it.title +// tv_chapter_url.visible() + } else { + tv_chapter_url.gone() + } + seek_read_page.max = it.pageSize.minus(1) + seek_read_page.progress = ReadBook.durPageIndex + tv_pre.isEnabled = ReadBook.durChapterIndex != 0 + tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 + } ?: let { + tv_chapter_name.gone() + tv_chapter_url.gone() + } + } + } + + override fun pageChanged() { + autoPageProgress = 0 + launch { + seek_read_page.progress = ReadBook.durPageIndex + } + } + + override fun contentLoadFinish() { + if (intent.getBooleanExtra("readAloud", false)) { + intent.removeExtra("readAloud") + ReadBook.readAloud() + } + loadStates = true + } + + override fun upSelectedStart(x: Float, y: Float, top: Float) { + cursor_left.x = x - cursor_left.width + cursor_left.y = y + cursor_left.visible(true) + text_menu_position.x = x + text_menu_position.y = top + } + + override fun upSelectedEnd(x: Float, y: Float) { + cursor_right.x = x + cursor_right.y = y + cursor_right.visible(true) + } + + override fun onCancelSelect() { + cursor_left.invisible() + cursor_right.invisible() + textActionMenu?.dismiss() + } + + private fun upScreenTimeOut() { + getPrefString(PreferKey.keepLight)?.let { + screenTimeOut = it.toLong() * 1000 + } + screenOffTimerStart() + } + + override fun screenOffTimerStart() { + if (screenTimeOut < 0) { + ReadBookActivityHelp.keepScreenOn(window, true) + return + } + val t = screenTimeOut - sysScreenOffTime + if (t > 0) { + mHandler.removeCallbacks(keepScreenRunnable) + ReadBookActivityHelp.keepScreenOn(window, true) + mHandler.postDelayed(keepScreenRunnable, screenTimeOut) + } else { + ReadBookActivityHelp.keepScreenOn(window, false) + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> textActionMenu?.dismiss() + MotionEvent.ACTION_MOVE -> { + when (v.id) { + R.id.cursor_left -> page_view.curPage.selectStartMove( + event.rawX + cursor_left.width, + event.rawY - cursor_left.height + ) + R.id.cursor_right -> page_view.curPage.selectEndMove( + event.rawX - cursor_right.width, + event.rawY - cursor_right.height + ) + } + } + MotionEvent.ACTION_UP -> showTextActionMenu() + } + return true + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + when { + ReadBookActivityHelp.isPrevKey(this, keyCode) -> { + if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { + page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV) + return true + } + } + ReadBookActivityHelp.isNextKey(this, keyCode) -> { + if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { + page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + return true + } + } + keyCode == KeyEvent.KEYCODE_VOLUME_UP -> { + if (volumeKeyPage(PageDelegate.Direction.PREV)) { + return true + } + } + keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (volumeKeyPage(PageDelegate.Direction.NEXT)) { + return true + } + } + keyCode == KeyEvent.KEYCODE_SPACE -> { + page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + return true + } + } + return super.onKeyDown(keyCode, event) + } + + /** + * 音量键翻页 + */ + private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean { + if (!read_menu.isVisible) { + if (getPrefBoolean("volumeKeyPage", true)) { + if (getPrefBoolean("volumeKeyPageOnPlay") + || BaseReadAloudService.pause + ) { + page_view.pageDelegate?.isCancel = false + page_view.pageDelegate?.keyTurnPage(direction) + return true + } + } + } + return false + } + + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.TIME_CHANGED) { page_view.upTime() } + observeEvent(EventBus.BATTERY_CHANGED) { page_view.upBattery(it) } + observeEvent(EventBus.OPEN_CHAPTER) { + viewModel.openChapter(it.chapterIndex, ReadBook.durPageIndex) + page_view.upContent() + } + observeEvent(EventBus.MEDIA_BUTTON) { + if (it) { + onClickReadAloud() + } else { + ReadBook.readAloud(!BaseReadAloudService.pause) + } + } + observeEvent(EventBus.UP_CONFIG) { + upSystemUiVisibility() + page_view.upBg() + page_view.upTipStyle() + page_view.upStyle() + if (it) { + ReadBook.loadContent(resetPageOffset = false) + } else { + page_view.upContent(resetPageOffset = false) + } + adView.setBackgroundColor(ReadBookConfig.bgMeanColor) + } + observeEvent(EventBus.ALOUD_STATE) { + if (it == Status.STOP || it == Status.PAUSE) { + ReadBook.curTextChapter?.let { textChapter -> + val page = textChapter.page(ReadBook.durPageIndex) + if (page != null) { + page.removePageAloudSpan() + page_view.upContent(resetPageOffset = false) + } + } + } + } + observeEventSticky(EventBus.TTS_PROGRESS) { chapterStart -> + launch(Dispatchers.IO) { + if (BaseReadAloudService.isPlay()) { + ReadBook.curTextChapter?.let { textChapter -> + val pageStart = + chapterStart - textChapter.getReadLength(ReadBook.durPageIndex) + textChapter.page(ReadBook.durPageIndex)?.upPageAloudSpan(pageStart) + upContent() + } + } + } + } + observeEvent(PreferKey.keepLight) { + upScreenTimeOut() + } + observeEvent(PreferKey.textSelectAble) { + page_view.curPage.upSelectAble(it) + } + observeEvent(PreferKey.showBrightnessView) { + setScreenBrightness(getPrefInt("brightness", 100)) + } + + } + + /** + * 设置屏幕亮度 + */ + private fun setScreenBrightness(value: Int) { + var brightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + if (!brightnessAuto()) { + brightness = value.toFloat() + if (brightness < 1f) brightness = 1f + brightness /= 255f + } + val params = window?.attributes + params?.screenBrightness = brightness + window?.attributes = params + } + + private fun brightnessAuto(): Boolean { + return getPrefBoolean("brightnessAuto", true) + } + + override fun autoPage() { + } + + override fun openReplaceRule() { + } + + override fun showMenuBar() { + read_menu.runMenuIn() + } + + override fun openChapterList() { + ReadBook.book?.let { + startActivityForResult( + requestCodeChapterList, + Pair("bookId", it.bookId.toString()) + ) + } + } + + override fun openSearchActivity(searchWord: String?) { + + } + + override fun showReadStyle() { + ReadStyleDialog().show(supportFragmentManager, "readStyle") + } + + override fun showAdjust() { + ReadAdjustDialog().show(supportFragmentManager, "readStyle") + } + + override fun showReadAloudDialog() { + ReadAloudDialog().show(supportFragmentManager, "readAloud") + } + + override fun upSystemUiVisibility() { + } + + override fun onClickReadAloud() { + when { + !BaseReadAloudService.isRun -> ReadBook.readAloud() + BaseReadAloudService.pause -> ReadAloud.resume(this) + else -> ReadAloud.pause(this) + } + } + + fun showBgTextConfig() { +// BgTextConfigDialog().show(supportFragmentManager, "bgTextConfig") + } + + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + requestCodeChapterList -> + data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> + if (index != ReadBook.durChapterIndex) { + val pageIndex = data.getIntExtra("pageIndex", 0) + viewModel.openChapter(index, pageIndex) + } + } + requestCodeSearchResult -> + data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> + launch(Dispatchers.IO) { + val indexWithinChapter = data.getIntExtra("indexWithinChapter", 0) + viewModel.searchContentQuery = data.getStringExtra("query") ?: "" + viewModel.openChapter(index) + // block until load correct chapter and pages + var pages = ReadBook.curTextChapter?.pages + while (ReadBook.durChapterIndex != index || pages == null) { + delay(100L) + pages = ReadBook.curTextChapter?.pages + } + val positions = + ReadBook.searchResultPositions( + pages, + indexWithinChapter, + viewModel.searchContentQuery + ) + while (ReadBook.durPageIndex != positions[0]) { + delay(100L) + ReadBook.skipToPage(positions[0]) + } + withContext(Dispatchers.Main) { + page_view.curPage.selectStartMoveIndex( + 0, + positions[1], + positions[2] + ) + delay(20L) + when (positions[3]) { + 0 -> page_view.curPage.selectEndMoveIndex( + 0, + positions[1], + positions[2] + viewModel.searchContentQuery.length - 1 + ) + 1 -> page_view.curPage.selectEndMoveIndex( + 0, + positions[1] + 1, + positions[4] + ) + //todo: consider change page, jump to scroll position + -1 -> page_view.curPage.selectEndMoveIndex(1, 0, positions[4]) + } + page_view.isTextSelected = true + delay(100L) + } + } + } + } + + } + } + + override fun onMenuItemSelected(itemId: Int): Boolean { + when (itemId) { + R.id.menu_search_content -> { + viewModel.searchContentQuery = selectedText + openSearchActivity(selectedText) + return true + } + } + return false + } + + override fun onMenuActionFinally() { + textActionMenu?.dismiss() + page_view.curPage.cancelSelect() + page_view.isTextSelected = false + } + + override fun finish() { + ReadBook.book?.let { + if (!ReadBook.inBookshelf) { + this.alert(title = getString(R.string.add_to_shelf)) { + message = getString(R.string.check_add_bookshelf, it.bookName) + okButton { + ReadBook.inBookshelf = true + setResult(Activity.RESULT_OK) + super.finish() + } + noButton { viewModel.removeFromBookshelf { super.finish() } } + }.show().applyTint() + } else { + super.finish() + } + } ?: super.finish() + } + + override fun onDestroy() { + super.onDestroy() + postEvent(EventBus.UP_BOOK, 0L) + mHandler.removeCallbacks(keepScreenRunnable) + textActionMenu?.dismiss() + page_view.onDestroy() + ReadBook.msg = null + if (!BuildConfig.DEBUG) { +// SyncBookProgress.uploadBookProgress() +// Backup.autoBack(this) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/ReadBookActivityHelp.kt b/app/src/main/java/com/novel/read/ui/read/ReadBookActivityHelp.kt new file mode 100644 index 0000000..51c3a30 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/ReadBookActivityHelp.kt @@ -0,0 +1,217 @@ +package com.novel.read.ui.read + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.pm.ActivityInfo +import android.os.AsyncTask +import android.os.Build +import android.view.LayoutInflater +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.widget.EditText +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.PreferKey +import com.novel.read.data.db.entity.Bookmark +import com.novel.read.help.AppConfig +import com.novel.read.help.ReadBookConfig +import com.novel.read.lib.ATH +import com.novel.read.lib.ThemeStore +import com.novel.read.lib.dialogs.* +import com.novel.read.service.help.CacheBook +import com.novel.read.service.help.ReadBook +import com.novel.read.utils.ext.applyTint +import com.novel.read.utils.ext.backgroundColor +import com.novel.read.utils.ext.getPrefString +import com.novel.read.utils.ext.requestInputMethod +import kotlinx.android.synthetic.main.dialog_download_choice.view.* +import kotlinx.android.synthetic.main.dialog_edit_text.view.* +import org.jetbrains.anko.layoutInflater + + +object ReadBookActivityHelp { + + /** + * 更新状态栏,导航栏 + */ + fun upSystemUiVisibility( + window: Window, + isInMultiWindow: Boolean, + toolBarHide: Boolean = true + ) { + var flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_IMMERSIVE + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) + if (!isInMultiWindow) { + flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } + if (ReadBookConfig.hideNavigationBar) { + flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + } + if (toolBarHide) { + if (ReadBookConfig.hideStatusBar) { + flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN + } + if (ReadBookConfig.hideNavigationBar) { + flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + } + } + window.decorView.systemUiVisibility = flag + if (toolBarHide) { + ATH.setLightStatusBar(window, ReadBookConfig.durConfig.curStatusIconDark()) + } else { + ATH.setLightStatusBarAuto( + window, + ThemeStore.statusBarColor(App.INSTANCE, AppConfig.isTransparentStatusBar) + ) + } + } + + /** + * 屏幕方向 + */ + @SuppressLint("SourceLockedOrientationActivity") + fun setOrientation(activity: Activity) = activity.apply { + when (AppConfig.requestedDirection) { + "0" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + "1" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + "2" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + "3" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR + } + } + + + /** + * 保持亮屏 + */ + fun keepScreenOn(window: Window, on: Boolean) { + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + /** + * 适配刘海 + */ + fun upLayoutInDisplayCutoutMode(window: Window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && AppConfig.readBodyToLh) { + window.attributes = window.attributes.apply { + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + } + + @SuppressLint("InflateParams") + fun showDownloadDialog(context: Context) { + ReadBook.book?.let { book -> + context.alert(titleResource = R.string.offline_cache) { + var view: View? = null + customView { + LayoutInflater.from(context).inflate(R.layout.dialog_download_choice, null) + .apply { + view = this + setBackgroundColor(context.backgroundColor) + edit_start.setText((book.durChapterIndex + 1).toString()) + edit_end.setText( + App.db.getChapterDao().getChapterCount(bookId = book.bookId).toString() + ) + } + } + yesButton { + view?.apply { + val start = edit_start?.text?.toString()?.toInt() ?: 0 + val end = edit_end?.text?.toString()?.toInt() ?: App.db.getChapterDao() + .getChapterCount(bookId = book.bookId) + CacheBook.start(context, book.bookId, start - 1, end - 1) + } + } + noButton() + }.show().applyTint() + } + } + + @SuppressLint("InflateParams") + fun showBookMark(context: Context) = with(context) { + val book = ReadBook.book ?: return + val textChapter = ReadBook.curTextChapter ?: return + context.alert(title = getString(R.string.bookmark_add)) { + var editText: EditText? = null + message = book.bookName + " • " + textChapter.title + customView { + layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { + editText = edit_view.apply { + setHint(R.string.note_content) + } + } + } + yesButton { + editText?.text?.toString()?.let { editContent -> + AsyncTask.execute { + val bookmark = Bookmark( + bookId = book.bookId, + bookName = book.bookName, + chapterIndex = ReadBook.durChapterIndex, + pageIndex = ReadBook.durPageIndex, + chapterName = textChapter.title, + content = editContent + ) + App.db.getBookMarkDao().insert(bookmark) + } + } + } + noButton() + }.show().applyTint().requestInputMethod() + } + + @SuppressLint("InflateParams") + fun showCharsetConfig(context: Context) = with(context) { +// val charsets = +// arrayListOf("UTF-8", "GB2312", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII") +// alert(R.string.set_charset) { +// var editText: AutoCompleteTextView? = null +// customView { +// layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { +// editText = edit_view +// edit_view.setFilterValues(charsets) +// edit_view.setText(ReadBook.book?.charset) +// } +// } +// okButton { +// val text = editText?.text?.toString() +// text?.let { +// ReadBook.setCharset(it) +// } +// } +// cancelButton() +// }.show().applyTint() + } + + fun showPageAnimConfig(context: Context, success: () -> Unit) = with(context) { + val items = arrayListOf() + items.add(getString(R.string.btn_default_s)) + items.add(getString(R.string.page_anim_cover)) + items.add(getString(R.string.page_anim_slide)) + items.add(getString(R.string.page_anim_simulation)) + items.add(getString(R.string.page_anim_scroll)) + items.add(getString(R.string.page_anim_none)) + selector(R.string.page_anim, items) { _, i -> + ReadBookConfig.pageAnim + success() + } + } + + fun isPrevKey(context: Context, keyCode: Int): Boolean { + val prevKeysStr = context.getPrefString(PreferKey.prevKeys) + return prevKeysStr?.split(",")?.contains(keyCode.toString()) ?: false + } + + fun isNextKey(context: Context, keyCode: Int): Boolean { + val nextKeysStr = context.getPrefString(PreferKey.nextKeys) + return nextKeysStr?.split(",")?.contains(keyCode.toString()) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/ReadBookViewModel.kt b/app/src/main/java/com/novel/read/ui/read/ReadBookViewModel.kt new file mode 100644 index 0000000..90f990f --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/ReadBookViewModel.kt @@ -0,0 +1,182 @@ +package com.novel.read.ui.read + +import android.app.Application +import android.content.Intent +import android.util.Log +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseViewModel +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.help.BookHelp +import com.novel.read.help.IntentDataHelp +import com.novel.read.network.repository.BookRepository +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.help.ReadAloud +import com.novel.read.service.help.ReadBook + +class ReadBookViewModel(application: Application) : BaseViewModel(application) { + private val bookRepository by lazy { BookRepository() } + var isInitFinish = false + var searchContentQuery = "" + + fun initData(intent: Intent) { + execute { + ReadBook.inBookshelf = intent.getBooleanExtra("inBookshelf", true) + IntentDataHelp.getData(intent.getStringExtra("key"))?.let { + initBook(it) + } ?: intent.getStringExtra("bookId")?.let { + App.db.getBookDao().getBook(it)?.let { book -> + initBook(book) + } + } ?: App.db.getBookDao().lastReadBook().let { + initBook(it) + } + }.onFinally { + if (ReadBook.inBookshelf) { + ReadBook.saveRead() + } + } + } + + private fun initBook(book: Book) { + if (ReadBook.book?.bookId != book.bookId) { + ReadBook.resetData(book) + isInitFinish = true + ReadBook.chapterSize = App.db.getChapterDao().getChapterCount(book.bookId) + if (ReadBook.chapterSize == 0 || !ReadBook.inBookshelf) { + loadChapterList(book) + } else { + if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) { + ReadBook.durChapterIndex = ReadBook.chapterSize - 1 + } + ReadBook.loadContent(resetPageOffset = true) + } + } else { + ReadBook.book = book + if (ReadBook.durChapterIndex != book.durChapterIndex) { + ReadBook.durChapterIndex = book.durChapterIndex + ReadBook.durPageIndex = book.durChapterPos + ReadBook.prevTextChapter = null + ReadBook.curTextChapter = null + ReadBook.nextTextChapter = null + } + ReadBook.titleDate.postValue(book.bookName) + isInitFinish = true + ReadBook.chapterSize = App.db.getChapterDao().getChapterCount(book.bookId) + if (ReadBook.chapterSize == 0 || !ReadBook.inBookshelf) { + loadChapterList(book) + } else { + if (ReadBook.curTextChapter != null) { + ReadBook.callBack?.upContent(resetPageOffset = false) + } else { + ReadBook.loadContent(resetPageOffset = true) + } + } + } + } + + fun loadChapterList( + book: Book, + changeDruChapterIndex: ((chapters: List) -> Unit)? = null + ) { + Log.e("loadChapterList", "loadChapterList: ") + if (book.isLocalBook()) { +// execute { +// LocalBook.getChapterList(book).let { +// App.db.getChapterDao().delByBook(book.bookUrl) +// App.db.getChapterDao().insert(*it.toTypedArray()) +// App.db.getBookDao().update(book) +// ReadBook.chapterSize = it.size +// if (it.isEmpty()) { +// ReadBook.upMsg(context.getString(R.string.error_load_toc)) +// } else { +// ReadBook.upMsg(null) +// ReadBook.loadContent(resetPageOffset = true) +// } +// } +// }.onError { +// ReadBook.upMsg("LoadTocError:${it.localizedMessage}") +// } + } else { + launch( + block = { + val bookChapters = bookRepository.getDirectory(book.bookId).chapterList + + val cList = arrayListOf() + for (chapter in bookChapters!!) { + val bookChapter = BookChapter( + bookId = chapter.bookId, + chapterId = chapter.chapterId, + chapterIndex = chapter.chapterIndex - 1, + chapterName = chapter.chapterName, + createTimeValue = chapter.createTime, + updateDate = "", + updateTimeValue = 0L, + chapterUrl = chapter.chapterUrl + ) + cList.add(bookChapter) + } + if (cList.isNotEmpty()) { + if (changeDruChapterIndex == null) { + App.db.getChapterDao().insert(cList.toTypedArray()) + book.totalChapterNum = cList.size + App.db.getBookDao().update(book) + ReadBook.chapterSize = cList.size + ReadBook.upMsg(null) + ReadBook.loadContent(resetPageOffset = true) + } else { + changeDruChapterIndex(cList) + } + } else { + ReadBook.upMsg(context.getString(R.string.error_load_toc)) + } + },error = { + ReadBook.upMsg(context.getString(R.string.error_load_toc)) + } + ) + } + } + + + fun openChapter(index: Int, pageIndex: Int = 0) { + ReadBook.prevTextChapter = null + ReadBook.curTextChapter = null + ReadBook.nextTextChapter = null + ReadBook.callBack?.upContent() + if (index != ReadBook.durChapterIndex) { + ReadBook.durChapterIndex = index + ReadBook.durPageIndex = pageIndex + } + ReadBook.saveRead() + ReadBook.loadContent(resetPageOffset = true) + } + + fun removeFromBookshelf(success: (() -> Unit)?) { + execute { + ReadBook.book?.delete() + }.onSuccess { + success?.invoke() + } + } + + + fun refreshContent(book: Book) { + execute { + App.db.getChapterDao().getChapter(book.bookId, ReadBook.durChapterIndex) + ?.let { chapter -> + BookHelp.delContent(book, chapter) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + } + + override fun onCleared() { + super.onCleared() + if (BaseReadAloudService.pause) { + ReadAloud.stop(context) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/ReadMenu.kt b/app/src/main/java/com/novel/read/ui/read/ReadMenu.kt new file mode 100644 index 0000000..b7ef875 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/ReadMenu.kt @@ -0,0 +1,243 @@ +package com.novel.read.ui.read + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.view.WindowManager +import android.view.animation.Animation +import android.widget.FrameLayout +import android.widget.SeekBar +import androidx.core.view.isVisible +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.PreferKey +import com.novel.read.help.AppConfig +import com.novel.read.help.ReadBookConfig +import com.novel.read.lib.Selector +import com.novel.read.service.help.ReadBook +import com.novel.read.utils.AnimationUtilsSupport +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.SystemUtils +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.view_read_menu.view.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.sdk27.listeners.onLongClick + +/** + * 阅读界面菜单 + */ +class ReadMenu @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + var cnaShowMenu: Boolean = false + private val callBack: CallBack? get() = activity as? CallBack + private lateinit var menuTopIn: Animation + private lateinit var menuTopOut: Animation + private lateinit var menuBottomIn: Animation + private lateinit var menuBottomOut: Animation + private val bgColor: Int = context.bottomBackground + private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + private val bottomBackgroundList: ColorStateList = Selector.colorBuild() + .setDefaultColor(bgColor) + .setPressedColor(ColorUtils.darkenColor(bgColor)) + .create() + private var onMenuOutEnd: (() -> Unit)? = null + val showBrightnessView get() = context.getPrefBoolean(PreferKey.showBrightnessView, true) + + init { + inflate(context, R.layout.view_read_menu, this) + if (AppConfig.isNightTheme) { + fabNightTheme.setImageResource(R.drawable.ic_daytime) + } else { + fabNightTheme.setImageResource(R.drawable.ic_brightness) + } + initAnimation() + val brightnessBackground = GradientDrawable() + brightnessBackground.cornerRadius = 5F.dp + brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f)) + ll_bottom_bg.setBackgroundColor(bgColor) + fabSearch.backgroundTintList = bottomBackgroundList + fabSearch.setColorFilter(textColor) + fabNightTheme.backgroundTintList = bottomBackgroundList + fabNightTheme.setColorFilter(textColor) + tv_pre.setTextColor(textColor) + tv_next.setTextColor(textColor) + iv_catalog.setColorFilter(textColor) + tv_catalog.setTextColor(textColor) + iv_read_aloud.setColorFilter(textColor) + tv_read_aloud.setTextColor(textColor) + iv_font.setColorFilter(textColor) + tv_font.setTextColor(textColor) + iv_setting.setColorFilter(textColor) + tv_setting.setTextColor(textColor) + vw_bg.onClick { } + vwNavigationBar.onClick { } + bindEvent() + } + + /** + * 设置屏幕亮度 + */ + private fun setScreenBrightness(value: Int) { + var brightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + if (!brightnessAuto()) { + brightness = value.toFloat() + if (brightness < 1f) brightness = 1f + brightness /= 255f + } + val params = activity?.window?.attributes + params?.screenBrightness = brightness + activity?.window?.attributes = params + } + + fun runMenuIn() { + this.visible() + title_bar.visible() + bottom_menu.visible() + title_bar.startAnimation(menuTopIn) + bottom_menu.startAnimation(menuBottomIn) + } + + fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) { + this.onMenuOutEnd = onMenuOutEnd + if (this.isVisible) { + title_bar.startAnimation(menuTopOut) + bottom_menu.startAnimation(menuBottomOut) + } + } + + private fun brightnessAuto(): Boolean { + return context.getPrefBoolean("brightnessAuto", true) || !showBrightnessView + } + + private fun bindEvent() { + + //阅读进度 + seek_read_page.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { + + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + ReadBook.skipToPage(seekBar.progress) + } + }) + + //搜索 + fabSearch.onClick { + runMenuOut { + callBack?.openSearchActivity(null) + } + } + + //夜间模式 + fabNightTheme.onClick { + AppConfig.isNightTheme = !AppConfig.isNightTheme + App.INSTANCE.applyDayNight() + } + + //上一章 + tv_pre.onClick { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } + + //下一章 + tv_next.onClick { ReadBook.moveToNextChapter(true) } + + //目录 + ll_catalog.onClick { + runMenuOut { + callBack?.openChapterList() + } + } + + //朗读 + ll_read_aloud.onClick { + runMenuOut { + callBack?.onClickReadAloud() + } + } + ll_read_aloud.onLongClick { + runMenuOut { callBack?.showReadAloudDialog() } + true + } + //界面 + ll_font.onClick { + runMenuOut { + callBack?.showAdjust() + } + } + + //设置 + ll_setting.onClick { + runMenuOut { + callBack?.showReadStyle() + } + } + } + + private fun initAnimation() { + //显示菜单 + menuTopIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_in) + menuBottomIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in) + menuTopIn.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + callBack?.upSystemUiVisibility() + } + + override fun onAnimationEnd(animation: Animation) { + vw_menu_bg.onClick { runMenuOut() } + vwNavigationBar.layoutParams = vwNavigationBar.layoutParams.apply { + height = + if (ReadBookConfig.hideNavigationBar + && SystemUtils.isNavigationBarExist(activity) + ) + context.navigationBarHeight + else 0 + } + } + + override fun onAnimationRepeat(animation: Animation) = Unit + }) + + //隐藏菜单 + menuTopOut = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_out) + menuBottomOut = + AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out) + menuTopOut.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + vw_menu_bg.setOnClickListener(null) + } + + override fun onAnimationEnd(animation: Animation) { + this@ReadMenu.invisible() + title_bar.invisible() + bottom_menu.invisible() + cnaShowMenu = false + onMenuOutEnd?.invoke() + callBack?.upSystemUiVisibility() + } + + override fun onAnimationRepeat(animation: Animation) = Unit + }) + } + + + + interface CallBack { + fun autoPage() + fun openReplaceRule() + fun openChapterList() + fun openSearchActivity(searchWord: String?) + fun showAdjust() + fun showReadStyle() + fun showReadAloudDialog() + fun upSystemUiVisibility() + fun onClickReadAloud() + } + +} diff --git a/app/src/main/java/com/novel/read/ui/read/TextActionMenu.kt b/app/src/main/java/com/novel/read/ui/read/TextActionMenu.kt new file mode 100644 index 0000000..c8c63cf --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/TextActionMenu.kt @@ -0,0 +1,194 @@ +package com.novel.read.ui.read + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.ResolveInfo +import android.os.Build +import android.speech.tts.TextToSpeech +import android.view.LayoutInflater +import android.view.Menu +import android.view.ViewGroup +import android.widget.PopupWindow +import androidx.annotation.RequiresApi +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.core.view.isVisible +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.service.BaseReadAloudService +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.item_text.view.* +import kotlinx.android.synthetic.main.popup_action_menu.view.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.toast +import java.util.* + +@SuppressLint("RestrictedApi") +class TextActionMenu(private val context: Context, private val callBack: CallBack) : + PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT), + TextToSpeech.OnInitListener { + + private val adapter = Adapter() + private val menu = MenuBuilder(context) + private val moreMenu = MenuBuilder(context) + + init { + @SuppressLint("InflateParams") + contentView = LayoutInflater.from(context).inflate(R.layout.popup_action_menu, null) + + isTouchable = true + isOutsideTouchable = false + isFocusable = false + + initRecyclerView() + setOnDismissListener { + contentView.apply { + iv_menu_more.setImageResource(R.drawable.ic_more_vert) + recycler_view_more.gone() + adapter.setList(menu.visibleItems) + recycler_view.visible() + } + } + } + + private fun initRecyclerView() = with(contentView) { + recycler_view.adapter = adapter + recycler_view_more.adapter = adapter + SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) + adapter.setList(menu.visibleItems) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + onInitializeMenu(moreMenu) + } + if (moreMenu.size() > 0) { + iv_menu_more.visible() + } + iv_menu_more.onClick { + if (recycler_view.isVisible) { + iv_menu_more.setImageResource(R.drawable.ic_arrow_back) + adapter.setList(menu.visibleItems) + recycler_view.gone() + recycler_view_more.visible() + } else { + iv_menu_more.setImageResource(R.drawable.ic_more_vert) + recycler_view_more.gone() + adapter.setList(menu.visibleItems) + recycler_view.visible() + } + } + } + + inner class Adapter : + BaseQuickAdapter(R.layout.item_text){ + + override fun convert(holder: BaseViewHolder, item: MenuItemImpl) { + holder.itemView.run { + text_view.text = item.title + } + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + if (!callBack.onMenuItemSelected(it.itemId)) { + onMenuItemSelected(it) + } + } + callBack.onMenuActionFinally() + } + } + + } + + private fun onMenuItemSelected(item: MenuItemImpl) { + when (item.itemId) { + R.id.menu_copy -> context.sendToClip(callBack.selectedText) + R.id.menu_aloud -> { + if (BaseReadAloudService.isRun) { + context.toast(R.string.alouding_disable) + return + } + readAloud(callBack.selectedText) + } + else -> item.intent?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText) + context.startActivity(it) + } + } + } + } + + private var textToSpeech: TextToSpeech? = null + private var ttsInitFinish = false + private var lastText: String = "" + + @SuppressLint("SetJavaScriptEnabled") + private fun readAloud(text: String) { + lastText = text + if (textToSpeech == null) { + textToSpeech = TextToSpeech(context, this) + return + } + if (!ttsInitFinish) return + if (text == "") return + if (textToSpeech?.isSpeaking == true) + textToSpeech?.stop() + textToSpeech?.speak(text, TextToSpeech.QUEUE_ADD, null, "select_text") + lastText = "" + } + + @Synchronized + override fun onInit(status: Int) { + textToSpeech?.language = Locale.CHINA + ttsInitFinish = true + readAloud(lastText) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun createProcessTextIntent(): Intent { + return Intent() + .setAction(Intent.ACTION_PROCESS_TEXT) + .setType("text/plain") + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getSupportedActivities(): List { + return context.packageManager + .queryIntentActivities(createProcessTextIntent(), 0) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun createProcessTextIntentForResolveInfo(info: ResolveInfo): Intent { + return createProcessTextIntent() + .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) + .setClassName(info.activityInfo.packageName, info.activityInfo.name) + } + + /** + * Start with a menu Item order value that is high enough + * so that your "PROCESS_TEXT" menu items appear after the + * standard selection menu items like Cut, Copy, Paste. + */ + @RequiresApi(Build.VERSION_CODES.M) + private fun onInitializeMenu(menu: Menu) { + try { + var menuItemOrder = 100 + for (resolveInfo in getSupportedActivities()) { + menu.add( + Menu.NONE, Menu.NONE, + menuItemOrder++, resolveInfo.loadLabel(context.packageManager) + ).intent = createProcessTextIntentForResolveInfo(resolveInfo) + } + } catch (e: Exception) { + context.toast("获取文字操作菜单出错:${e.localizedMessage}") + } + } + + interface CallBack { + val selectedText: String + + fun onMenuItemSelected(itemId: Int): Boolean + + fun onMenuActionFinally() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/config/ChineseConverter.kt b/app/src/main/java/com/novel/read/ui/read/config/ChineseConverter.kt new file mode 100644 index 0000000..574b929 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/config/ChineseConverter.kt @@ -0,0 +1,54 @@ +package com.novel.read.ui.read.config + +import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.AttributeSet +import com.novel.read.R +import com.novel.read.help.AppConfig +import com.novel.read.lib.dialogs.alert +import com.novel.read.ui.widget.StrokeTextView +import com.novel.read.utils.ext.accentColor +import com.novel.read.utils.ext.applyTint +import org.jetbrains.anko.sdk27.listeners.onClick + +class ChineseConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) { + + private val spannableString = SpannableString("简/繁") + private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor) + private var onChanged: (() -> Unit)? = null + + init { + text = spannableString + if (!isInEditMode) { + upUi(AppConfig.chineseConverterType) + } + onClick { + selectType() + } + } + + private fun upUi(type: Int) { + spannableString.removeSpan(enabledSpan) + when (type) { + 1 -> spannableString.setSpan(enabledSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 2 -> spannableString.setSpan(enabledSpan, 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + text = spannableString + } + + private fun selectType() { + context.alert(titleResource = R.string.chinese_converter) { + items(context.resources.getStringArray(R.array.chinese_mode).toList()) { _, i -> + AppConfig.chineseConverterType = i + upUi(i) + onChanged?.invoke() + } + }.show().applyTint() + } + + fun onChanged(unit: () -> Unit) { + onChanged = unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/config/ReadAdjustDialog.kt b/app/src/main/java/com/novel/read/ui/read/config/ReadAdjustDialog.kt new file mode 100644 index 0000000..db76f42 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/config/ReadAdjustDialog.kt @@ -0,0 +1,118 @@ +package com.novel.read.ui.read.config + +import android.os.Bundle +import android.view.* +import android.widget.SeekBar +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseDialogFragment +import com.novel.read.constant.PreferKey +import com.novel.read.help.AppConfig +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.dialog_read_adjust.* + +class ReadAdjustDialog :BaseDialogFragment() { + + val callBack get() = activity as? ReadBookActivity + val showBrightnessView get() = context?.getPrefBoolean(PreferKey.showBrightnessView, true) + + override fun onStart() { + super.onStart() + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 0, 0, 0) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + it.attributes = attr + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_read_adjust, container) + } + + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + initData() + initViewEvent() + } + + private fun initView() { + } + + private fun initData() { + scb_follow_sys.isChecked = brightnessAuto() + hpb_light.progress = requireContext().getPrefInt("brightness", 100) + upBrightnessState() + + swt_dark.isChecked=AppConfig.isNightTheme + + swt_dark.setOnCheckedChangeListener { compoundButton, b -> + AppConfig.isNightTheme = b + App.INSTANCE.applyDayNight() + } + } + + private fun initViewEvent() { + //亮度调节 + scb_follow_sys.setOnClickListener { + if (scb_follow_sys.isChecked) { + scb_follow_sys.setChecked(checked = false, animate = true) + } else { + scb_follow_sys.setChecked(checked = true, animate = true) + } + context?.putPrefBoolean("brightnessAuto", !brightnessAuto()) + upBrightnessState() + } + + //亮度调节 + hpb_light.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + setScreenBrightness(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + context?.putPrefInt("brightness", hpb_light.progress) + } + + }) + } + + private fun upBrightnessState() { + hpb_light.isEnabled = !brightnessAuto() + context?.getPrefInt("brightness", 100)?.let { setScreenBrightness(it) } + } + + + /** + * 设置屏幕亮度 + */ + private fun setScreenBrightness(value: Int) { + var brightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + if (!brightnessAuto()) { + brightness = value.toFloat() + if (brightness < 1f) brightness = 1f + brightness /= 255f + } + val params = activity?.window?.attributes + params?.screenBrightness = brightness + activity?.window?.attributes = params + } + + private fun brightnessAuto(): Boolean { + return context?.getPrefBoolean("brightnessAuto", true)!! || !showBrightnessView!! + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/config/ReadAloudDialog.kt b/app/src/main/java/com/novel/read/ui/read/config/ReadAloudDialog.kt new file mode 100644 index 0000000..b87ce89 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/config/ReadAloudDialog.kt @@ -0,0 +1,164 @@ +package com.novel.read.ui.read.config + +import android.os.Bundle +import android.view.* +import android.widget.SeekBar +import com.novel.read.R +import com.novel.read.base.BaseDialogFragment +import com.novel.read.constant.EventBus +import com.novel.read.help.AppConfig +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.help.ReadAloud +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.widget.dialog.ReadAloudConfigDialog +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.dialog_read_aloud.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class ReadAloudDialog : BaseDialogFragment() { + var callBack: CallBack? = null + + override fun onStart() { + super.onStart() + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 0, 0, 0) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + it.attributes = attr + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + callBack = activity as? CallBack + return inflater.inflate(R.layout.dialog_read_aloud, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + root_view.setBackgroundColor(bg) + tv_pre.setTextColor(textColor) + tv_next.setTextColor(textColor) + iv_play_prev.setColorFilter(textColor) + iv_play_pause.setColorFilter(textColor) + iv_play_next.setColorFilter(textColor) + iv_stop.setColorFilter(textColor) + iv_timer.setColorFilter(textColor) + tv_timer.setTextColor(textColor) + tv_tts_speed.setTextColor(textColor) + iv_catalog.setColorFilter(textColor) + tv_catalog.setTextColor(textColor) + iv_main_menu.setColorFilter(textColor) + tv_main_menu.setTextColor(textColor) + iv_to_backstage.setColorFilter(textColor) + tv_to_backstage.setTextColor(textColor) + iv_setting.setColorFilter(textColor) + tv_setting.setTextColor(textColor) + cb_tts_follow_sys.setTextColor(textColor) + initOnChange() + initData() + initEvent() + } + + private fun initData() { + upPlayState() + upTimerText(BaseReadAloudService.timeMinute) + seek_timer.progress = BaseReadAloudService.timeMinute + cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) + seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked + seek_tts_SpeechRate.progress = AppConfig.ttsSpeechRate + } + + private fun initOnChange() { + cb_tts_follow_sys.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + requireContext().putPrefBoolean("ttsFollowSys", isChecked) + seek_tts_SpeechRate.isEnabled = !isChecked + upTtsSpeechRate() + } + } + seek_tts_SpeechRate.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + AppConfig.ttsSpeechRate = seek_tts_SpeechRate.progress + upTtsSpeechRate() + } + }) + seek_timer.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + upTimerText(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + ReadAloud.setTimer(requireContext(), seek_timer.progress) + } + }) + } + + private fun initEvent() { + ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() } + ll_setting.onClick { + ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") + } + tv_pre.onClick { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } + tv_next.onClick { ReadBook.moveToNextChapter(true) } + iv_stop.onClick { ReadAloud.stop(requireContext()); dismiss() } + iv_play_pause.onClick { callBack?.onClickReadAloud() } + iv_play_prev.onClick { ReadAloud.prevParagraph(requireContext()) } + iv_play_next.onClick { ReadAloud.nextParagraph(requireContext()) } + ll_catalog.onClick { callBack?.openChapterList() } + ll_to_backstage.onClick { callBack?.finish() } + } + + private fun upPlayState() { + if (!BaseReadAloudService.pause) { + iv_play_pause.setImageResource(R.drawable.ic_pause_24dp) + } else { + iv_play_pause.setImageResource(R.drawable.ic_play_24dp) + } + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + iv_play_pause.setColorFilter(textColor) + } + + private fun upTimerText(timeMinute: Int) { + tv_timer.text = requireContext().getString(R.string.timer_m, timeMinute) + } + + private fun upTtsSpeechRate() { + ReadAloud.upTtsSpeechRate(requireContext()) + if (!BaseReadAloudService.pause) { + ReadAloud.pause(requireContext()) + ReadAloud.resume(requireContext()) + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.ALOUD_STATE) { upPlayState() } + observeEvent(EventBus.TTS_DS) { seek_timer.progress = it } + } + + interface CallBack { + fun showMenuBar() + fun openChapterList() + fun onClickReadAloud() + fun finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/config/ReadStyleDialog.kt b/app/src/main/java/com/novel/read/ui/read/config/ReadStyleDialog.kt new file mode 100644 index 0000000..9caef1d --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/config/ReadStyleDialog.kt @@ -0,0 +1,170 @@ +package com.novel.read.ui.read.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.* +import androidx.core.view.get +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.base.BaseDialogFragment +import com.novel.read.constant.EventBus +import com.novel.read.help.ReadBookConfig +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.dialog_read_book_style.* +import kotlinx.android.synthetic.main.activity_read_book.* +import kotlinx.android.synthetic.main.item_read_style.view.* +import org.jetbrains.anko.sdk27.listeners.onCheckedChange +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.sdk27.listeners.onLongClick + +class ReadStyleDialog : BaseDialogFragment() { + + val callBack get() = activity as? ReadBookActivity + private lateinit var styleAdapter: StyleAdapter + + + override fun onStart() { + super.onStart() + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 0, 0, 0) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + it.attributes = attr + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_read_book_style, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + initData() + initViewEvent() + } + + private fun initView() { + val bg = requireContext().bottomBackground + root_view.setBackgroundColor(bg) + styleAdapter = StyleAdapter() + rv_style.adapter = styleAdapter + + } + + private fun initData() { + upView() + styleAdapter.setList(ReadBookConfig.configList) + } + + private fun initViewEvent() { + tv_font_type.onChanged { + postEvent(EventBus.UP_CONFIG, true) + } + + fl_text_Bold.onChanged { + postEvent(EventBus.UP_CONFIG, true) + } + + fl_text_font.onClick { +// FontSelectDialog().show(childFragmentManager, "fontSelectDialog") + } + + rg_page_anim.onCheckedChange { _, checkedId -> + ReadBookConfig.pageAnim = -1 + ReadBookConfig.pageAnim = rg_page_anim.getIndexById(checkedId) + callBack?.page_view?.upPageAnim() + } + + nbTextSizeAdd.setOnClickListener { + ReadBookConfig.textSize = ReadBookConfig.textSize + 2 + postEvent(EventBus.UP_CONFIG, true) + nbTextSize.text = ReadBookConfig.textSize.toString() + } + nbTextSizeDec.setOnClickListener { + ReadBookConfig.textSize = ReadBookConfig.textSize - 2 + postEvent(EventBus.UP_CONFIG, true) + nbTextSize.text = ReadBookConfig.textSize.toString() + } + } + + + inner class StyleAdapter : + BaseQuickAdapter(R.layout.item_read_style) { + + override fun convert(holder: BaseViewHolder, item: ReadBookConfig.Config) { + holder.itemView.run { + iv_style.setText(item.name.ifBlank { "文字" }) + iv_style.setTextColor(item.curTextColor()) + iv_style.setImageDrawable(item.curBgDrawable(100, 150)) + if (ReadBookConfig.styleSelect == holder.layoutPosition) { + iv_style.borderColor = accentColor + iv_style.setTextBold(true) + } else { + iv_style.borderColor = item.curTextColor() + iv_style.setTextBold(false) + } + iv_style.onClick { + if (iv_style.isInView) { + changeBg(holder.layoutPosition) + } + } + iv_style.onLongClick { + if (iv_style.isInView) { + showBgTextConfig(holder.layoutPosition) + } else { + false + } + } + } + } + } + + + private fun changeBg(index: Int) { + val oldIndex = ReadBookConfig.styleSelect + if (index != oldIndex) { + ReadBookConfig.styleSelect = index + ReadBookConfig.upBg() + upView() + styleAdapter.notifyItemChanged(oldIndex) + styleAdapter.notifyItemChanged(index) + postEvent(EventBus.UP_CONFIG, true) + } + } + + private fun showBgTextConfig(index: Int): Boolean { + dismiss() + changeBg(index) + callBack?.showBgTextConfig() + return true + } + + private fun upView() { + + ReadBook.pageAnim().let { + if (it >= 0 && it < rg_page_anim.childCount) { + rg_page_anim.check(rg_page_anim[it].id) + } + } + ReadBookConfig.let { + + } + } + + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + ReadBookConfig.save() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/config/TextFontWeightConverter.kt b/app/src/main/java/com/novel/read/ui/read/config/TextFontWeightConverter.kt new file mode 100644 index 0000000..9bc99c4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/config/TextFontWeightConverter.kt @@ -0,0 +1,55 @@ +package com.novel.read.ui.read.config + +import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.AttributeSet +import com.novel.read.R +import com.novel.read.help.ReadBookConfig +import com.novel.read.lib.dialogs.alert +import com.novel.read.ui.widget.StrokeTextView +import com.novel.read.utils.ext.accentColor +import com.novel.read.utils.ext.applyTint +import org.jetbrains.anko.sdk27.listeners.onClick + +class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) { + + private val spannableString = SpannableString("中/粗/细") + private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor) + private var onChanged: (() -> Unit)? = null + + init { + text = spannableString + if (!isInEditMode) { + upUi(ReadBookConfig.textBold) + } + onClick { + selectType() + } + } + + private fun upUi(type: Int) { + spannableString.removeSpan(enabledSpan) + when (type) { + 0 -> spannableString.setSpan(enabledSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 1 -> spannableString.setSpan(enabledSpan, 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 2 -> spannableString.setSpan(enabledSpan, 4, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + text = spannableString + } + + private fun selectType() { + context.alert(titleResource = R.string.text_font_weight_converter) { + items(context.resources.getStringArray(R.array.text_font_weight).toList()) { _, i -> + ReadBookConfig.textBold = i + upUi(i) + onChanged?.invoke() + } + }.show().applyTint() + } + + fun onChanged(unit: () -> Unit) { + onChanged = unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/ContentTextView.kt b/app/src/main/java/com/novel/read/ui/read/page/ContentTextView.kt new file mode 100644 index 0000000..fe8c13d --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/ContentTextView.kt @@ -0,0 +1,504 @@ +package com.novel.read.ui.read.page + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.util.Log +import android.view.View +import com.novel.read.R +import com.novel.read.constant.PreferKey +import com.novel.read.data.read.TextChar +import com.novel.read.data.read.TextLine +import com.novel.read.data.read.TextPage +import com.novel.read.help.ReadBookConfig +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.widget.dialog.PhotoDialog +import com.novel.read.utils.ext.* +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.ImageProvider + +import kotlinx.coroutines.CoroutineScope +import kotlin.math.min + +/** + * 阅读内容界面 + */ +class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { + var selectAble = context.getPrefBoolean(PreferKey.textSelectAble,true) + var upView: ((TextPage) -> Unit)? = null + private val selectedPaint by lazy { + Paint().apply { + color = context.getCompatColor(R.color.btn_bg_press_2) + style = Paint.Style.FILL + } + } + private var callBack: CallBack + private val visibleRect = RectF() + private val selectStart = arrayOf(0, 0, 0) + private val selectEnd = arrayOf(0, 0, 0) + private var textPage: TextPage = TextPage() + + //滚动参数 + private val pageFactory: TextPageFactory get() = callBack.pageFactory + private var pageOffset = 0f + + init { + callBack = activity as CallBack + contentDescription = textPage.text + } + + fun setContent(textPage: TextPage) { + this.textPage = textPage + contentDescription = textPage.text + invalidate() + } + + fun upVisibleRect() { + visibleRect.set( + ChapterProvider.paddingLeft.toFloat(), + ChapterProvider.paddingTop.toFloat(), + ChapterProvider.visibleRight.toFloat(), + ChapterProvider.visibleBottom.toFloat() + ) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + ChapterProvider.upViewSize(w, h) + upVisibleRect() + textPage.format() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.clipRect(visibleRect) + drawPage(canvas) + } + + /** + * 绘制页面 + */ + private fun drawPage(canvas: Canvas) { + var relativeOffset = relativeOffset(0) + textPage.textLines.forEach { textLine -> + draw(canvas, textLine, relativeOffset) + } + if (!callBack.isScroll) return + //滚动翻页 + if (!pageFactory.hasNext()) return + val nextPage = relativePage(1) + relativeOffset = relativeOffset(1) + nextPage.textLines.forEach { textLine -> + draw(canvas, textLine, relativeOffset) + } + if (!pageFactory.hasNextPlus()) return + relativeOffset = relativeOffset(2) + if (relativeOffset < ChapterProvider.visibleHeight) { + relativePage(2).textLines.forEach { textLine -> + draw(canvas, textLine, relativeOffset) + } + } + } + + private fun draw( + canvas: Canvas, + textLine: TextLine, + relativeOffset: Float + ) { + val lineTop = textLine.lineTop + relativeOffset + val lineBase = textLine.lineBase + relativeOffset + val lineBottom = textLine.lineBottom + relativeOffset + if (textLine.isImage) { + drawImage(canvas, textLine, lineTop, lineBottom) + } else { + drawChars( + canvas, + textLine.textChars, + lineTop, + lineBase, + lineBottom, + isTitle = textLine.isTitle, + isReadAloud = textLine.isReadAloud + ) + } + } + + /** + * 绘制文字 + */ + private fun drawChars( + canvas: Canvas, + textChars: List, + lineTop: Float, + lineBase: Float, + lineBottom: Float, + isTitle: Boolean, + isReadAloud: Boolean + ) { + val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint + textPaint.color = + if (isReadAloud) context.accentColor else ReadBookConfig.textColor + textChars.forEach { + canvas.drawText(it.charData, it.start, lineBase, textPaint) + if (it.selected) { + canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint) + } + } + } + + /** + * 绘制图片 + */ + private fun drawImage( + canvas: Canvas, + textLine: TextLine, + lineTop: Float, + lineBottom: Float + ) { + textLine.textChars.forEach { textChar -> + ReadBook.book?.let { book -> + val rectF = RectF(textChar.start, lineTop, textChar.end, lineBottom) + ImageProvider.getImage(book, textPage.chapterIndex, textChar.charData, true) + ?.let { + canvas.drawBitmap(it, null, rectF, null) + } + } + } + } + + /** + * 滚动事件 + */ + fun onScroll(mOffset: Float) { + if (mOffset == 0f) return + pageOffset += mOffset + if (!pageFactory.hasPrev() && pageOffset > 0) { + pageOffset = 0f + } else if (!pageFactory.hasNext() + && pageOffset < 0 + && pageOffset + textPage.height < ChapterProvider.visibleHeight + ) { + val offset = ChapterProvider.visibleHeight - textPage.height + pageOffset = min(0f, offset) + } else if (pageOffset > 0) { + pageFactory.moveToPrev(false) + textPage = pageFactory.currentPage + pageOffset -= textPage.height + upView?.invoke(textPage) + } else if (pageOffset < -textPage.height) { + pageOffset += textPage.height + pageFactory.moveToNext(false) + textPage = pageFactory.currentPage + upView?.invoke(textPage) + } + invalidate() + } + + fun resetPageOffset() { + pageOffset = 0f + } + + /** + * 选择文字 + */ + fun selectText( + x: Float, + y: Float, + select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit + ) { + if (!selectAble) return + if (!visibleRect.contains(x, y)) return + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) return + if (relativeOffset >= ChapterProvider.visibleHeight) return + } + val page = relativePage(relativePos) + for ((lineIndex, textLine) in page.textLines.withIndex()) { + if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.start && x < textChar.end) { + if (textChar.isImage) { + activity?.supportFragmentManager?.let { + PhotoDialog.show(it, page.chapterIndex, textChar.charData) + } + } else { + textChar.selected = true + invalidate() + select(relativePos, lineIndex, charIndex) + } + return + } + } + return + } + } + + } + } + + /** + * 开始选择符移动 + */ + fun selectStartMove(x: Float, y: Float) { + if (!visibleRect.contains(x, y)) return + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) return + if (relativeOffset >= ChapterProvider.visibleHeight) return + } + for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { + if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.start && x < textChar.end) { + if (selectStart[0] != relativePos || selectStart[1] != lineIndex || selectStart[2] != charIndex) { + if (selectToInt(relativePos, lineIndex, charIndex) > selectToInt( + selectEnd + ) + ) { + return + } + selectStart[0] = relativePos + selectStart[1] = lineIndex + selectStart[2] = charIndex + upSelectedStart( + textChar.start, + textLine.lineBottom + relativeOffset, + textLine.lineTop + relativeOffset + ) + upSelectChars() + } + return + } + } + return + } + } + } + } + + /** + * 结束选择符移动 + */ + fun selectEndMove(x: Float, y: Float) { + if (!visibleRect.contains(x, y)) return + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) return + if (relativeOffset >= ChapterProvider.visibleHeight) return + } + Log.e("y", "$y") + for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { + if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { + Log.e("line", "$relativePos $lineIndex") + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.start && x < textChar.end) { + Log.e("char", "$relativePos $lineIndex $charIndex") + if (selectEnd[0] != relativePos || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) { + if (selectToInt(relativePos, lineIndex, charIndex) < selectToInt( + selectStart + ) + ) { + return + } + selectEnd[0] = relativePos + selectEnd[1] = lineIndex + selectEnd[2] = charIndex + upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) + upSelectChars() + } + return + } + } + return + } + } + } + } + + /** + * 选择开始文字 + */ + fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { + selectStart[0] = relativePage + selectStart[1] = lineIndex + selectStart[2] = charIndex + val textLine = relativePage(relativePage).textLines[lineIndex] + val textChar = textLine.textChars[charIndex] + upSelectedStart( + textChar.start, + textLine.lineBottom + relativeOffset(relativePage), + textLine.lineTop + relativeOffset(relativePage) + ) + upSelectChars() + } + + /** + * 选择结束文字 + */ + fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { + selectEnd[0] = relativePage + selectEnd[1] = lineIndex + selectEnd[2] = charIndex + val textLine = relativePage(relativePage).textLines[lineIndex] + val textChar = textLine.textChars[charIndex] + upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage)) + upSelectChars() + } + + private fun upSelectChars() { + val last = if (callBack.isScroll) 2 else 0 + for (relativePos in 0..last) { + for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + textChar.selected = + if (relativePos == selectStart[0] + && relativePos == selectEnd[0] + && lineIndex == selectStart[1] + && lineIndex == selectEnd[1] + ) { + charIndex in selectStart[2]..selectEnd[2] + } else if (relativePos == selectStart[0] && lineIndex == selectStart[1]) { + charIndex >= selectStart[2] + } else if (relativePos == selectEnd[0] && lineIndex == selectEnd[1]) { + charIndex <= selectEnd[2] + } else if (relativePos == selectStart[0] && relativePos == selectEnd[0]) { + lineIndex in (selectStart[1] + 1) until selectEnd[1] + } else if (relativePos == selectStart[0]) { + lineIndex > selectStart[1] + } else if (relativePos == selectEnd[0]) { + lineIndex < selectEnd[1] + } else { + relativePos in selectStart[0] + 1 until selectEnd[0] + } + } + } + } + invalidate() + } + + private fun upSelectedStart(x: Float, y: Float, top: Float) = callBack.apply { + upSelectedStart(x, y + headerHeight, top + headerHeight) + } + + private fun upSelectedEnd(x: Float, y: Float) = callBack.apply { + upSelectedEnd(x, y + headerHeight) + } + + fun cancelSelect() { + val last = if (callBack.isScroll) 2 else 0 + for (relativePos in 0..last) { + relativePage(relativePos).textLines.forEach { textLine -> + textLine.textChars.forEach { + it.selected = false + } + } + } + invalidate() + callBack.onCancelSelect() + } + + val selectedText: String + get() { + val stringBuilder = StringBuilder() + for (relativePos in selectStart[0]..selectEnd[0]) { + val textPage = relativePage(relativePos) + if (relativePos == selectStart[0] && relativePos == selectEnd[0]) { + for (lineIndex in selectStart[1]..selectEnd[1]) { + if (lineIndex == selectStart[1] && lineIndex == selectEnd[1]) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring( + selectStart[2], + selectEnd[2] + 1 + ) + ) + } else if (lineIndex == selectStart[1]) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring( + selectStart[2] + ) + ) + } else if (lineIndex == selectEnd[1]) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1) + ) + } else { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } + } else if (relativePos == selectStart[0]) { + for (lineIndex in selectStart[1] until relativePage(relativePos).textLines.size) { + if (lineIndex == selectStart[1]) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring( + selectStart[2] + ) + ) + } else { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } + } else if (relativePos == selectEnd[0]) { + for (lineIndex in 0..selectEnd[1]) { + if (lineIndex == selectEnd[1]) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1) + ) + } else { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } + } else if (relativePos in selectStart[0] + 1 until selectEnd[0]) { + for (lineIndex in selectStart[1]..selectEnd[1]) { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } + } + return stringBuilder.toString() + } + + private fun selectToInt(page: Int, line: Int, char: Int): Int { + return page * 10000000 + line * 100000 + char + } + + private fun selectToInt(select: Array): Int { + return select[0] * 10000000 + select[1] * 100000 + select[2] + } + + private fun relativeOffset(relativePos: Int): Float { + return when (relativePos) { + 0 -> pageOffset + 1 -> pageOffset + textPage.height + else -> pageOffset + textPage.height + pageFactory.nextPage.height + } + } + + private fun relativePage(relativePos: Int): TextPage { + return when (relativePos) { + 0 -> textPage + 1 -> pageFactory.nextPage + else -> pageFactory.nextPagePlus + } + } + + interface CallBack { + fun upSelectedStart(x: Float, y: Float, top: Float) + fun upSelectedEnd(x: Float, y: Float) + fun onCancelSelect() + val headerHeight: Int + val pageFactory: TextPageFactory + val scope: CoroutineScope + val isScroll: Boolean + } +} diff --git a/app/src/main/java/com/novel/read/ui/read/page/ContentView.kt b/app/src/main/java/com/novel/read/ui/read/page/ContentView.kt new file mode 100644 index 0000000..c6bb156 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/ContentView.kt @@ -0,0 +1,277 @@ +package com.novel.read.ui.read.page + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.widget.FrameLayout +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import com.novel.read.R +import com.novel.read.base.BaseActivity +import com.novel.read.constant.AppConst.timeFormat +import com.novel.read.data.read.TextPage +import com.novel.read.help.ReadBookConfig +import com.novel.read.help.ReadTipConfig +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.widget.BatteryView +import com.novel.read.utils.ext.* +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import kotlinx.android.synthetic.main.view_book_page.view.* +import org.jetbrains.anko.topPadding +import java.util.* + + +class ContentView(context: Context) : FrameLayout(context) { + + private var battery = 100 + private var tvTitle: BatteryView? = null + private var tvTime: BatteryView? = null + private var tvBattery: BatteryView? = null + private var tvPage: BatteryView? = null + private var tvTotalProgress: BatteryView? = null + private var tvPageAndTotal: BatteryView? = null + private var tvBookName: BatteryView? = null + + val headerHeight: Int + get() { + val h1 = if (ReadBookConfig.hideStatusBar) 0 else context.statusBarHeight + val h2 = if (ll_header.isGone) 0 else ll_header.height + return h1 + h2 + } + + init { + //设置背景防止切换背景时文字重叠 + setBackgroundColor(context.getCompatColor(R.color.background)) + inflate(context, R.layout.view_book_page, this) + upTipStyle() + upStyle() + content_text_view.upView = { + setProgress(it) + } + } + + fun upStyle() { + ReadBookConfig.apply { + bv_header_left.typeface = ChapterProvider.typeface + tv_header_left.typeface = ChapterProvider.typeface + tv_header_middle.typeface = ChapterProvider.typeface + tv_header_right.typeface = ChapterProvider.typeface + bv_footer_left.typeface = ChapterProvider.typeface + tv_footer_left.typeface = ChapterProvider.typeface + tv_footer_middle.typeface = ChapterProvider.typeface + tv_footer_right.typeface = ChapterProvider.typeface + bv_header_left.setColor(textColor) + tv_header_left.setColor(textColor) + tv_header_middle.setColor(textColor) + tv_header_right.setColor(textColor) + bv_footer_left.setColor(textColor) + tv_footer_left.setColor(textColor) + tv_footer_middle.setColor(textColor) + tv_footer_right.setColor(textColor) + upStatusBar() + ll_header.setPadding( + headerPaddingLeft.dp, + headerPaddingTop.dp, + headerPaddingRight.dp, + headerPaddingBottom.dp + ) + ll_footer.setPadding( + footerPaddingLeft.dp, + footerPaddingTop.dp, + footerPaddingRight.dp, + footerPaddingBottom.dp + ) + vw_top_divider.visible(showHeaderLine) + vw_bottom_divider.visible(showFooterLine) + content_text_view.upVisibleRect() + } + upTime() + upBattery(battery) + } + + /** + * 显示状态栏时隐藏header + */ + fun upStatusBar() { + vw_status_bar.topPadding = context.statusBarHeight + vw_status_bar.isGone = + ReadBookConfig.hideStatusBar || (activity as? BaseActivity)?.isInMultiWindow == true + } + + fun upTipStyle() { + ReadTipConfig.apply { + tv_header_left.isInvisible = tipHeaderLeft != chapterTitle + bv_header_left.isInvisible = tipHeaderLeft == none || !tv_header_left.isInvisible + tv_header_right.isGone = tipHeaderRight == none + tv_header_middle.isGone = tipHeaderMiddle == none + tv_footer_left.isInvisible = tipFooterLeft != chapterTitle + bv_footer_left.isInvisible = tipFooterLeft == none || !tv_footer_left.isInvisible + tv_footer_right.isGone = tipFooterRight == none + tv_footer_middle.isGone = tipFooterMiddle == none + ll_header.isGone = hideHeader + ll_footer.isGone = hideFooter + } + tvTitle = when (ReadTipConfig.chapterTitle) { + ReadTipConfig.tipHeaderLeft -> tv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> tv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvTitle?.apply { + isBattery = false + textSize = 12f + } + tvTime = when (ReadTipConfig.time) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvTime?.apply { + isBattery = false + textSize = 12f + } + tvBattery = when (ReadTipConfig.battery) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvBattery?.apply { + isBattery = true + textSize = 10f + } + tvPage = when (ReadTipConfig.page) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvPage?.apply { + isBattery = false + textSize = 12f + } + tvTotalProgress = when (ReadTipConfig.totalProgress) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvTotalProgress?.apply { + isBattery = false + textSize = 12f + } + tvPageAndTotal = when (ReadTipConfig.pageAndTotal) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvPageAndTotal?.apply { + isBattery = false + textSize = 12f + } + tvBookName = when (ReadTipConfig.bookName) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvBookName?.apply { + isBattery = false + textSize = 12f + } + } + + fun setBg(bg: Drawable?) { + page_panel.background = bg + } + + fun upTime() { + tvTime?.text = timeFormat.format(Date(System.currentTimeMillis())) + } + + fun upBattery(battery: Int) { + this.battery = battery + tvBattery?.setBattery(battery) + } + + fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { + setProgress(textPage) + if (resetPageOffset) + resetPageOffset() + content_text_view.setContent(textPage) + } + + fun resetPageOffset() { + content_text_view.resetPageOffset() + } + + @SuppressLint("SetTextI18n") + fun setProgress(textPage: TextPage) = textPage.apply { + tvBookName?.text = ReadBook.book?.bookName + tvTitle?.text = textPage.title + tvPage?.text = "${index.plus(1)}/$pageSize" + tvTotalProgress?.text = readProgress + tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" + } + + fun onScroll(offset: Float) { + content_text_view.onScroll(offset) + } + + fun upSelectAble(selectAble: Boolean) { + content_text_view.selectAble = selectAble + } + + fun selectText( + x: Float, y: Float, + select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit + ) { + return content_text_view.selectText(x, y - headerHeight, select) + } + + fun selectStartMove(x: Float, y: Float) { + content_text_view.selectStartMove(x, y - headerHeight) + } + + fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { + content_text_view.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + + fun selectEndMove(x: Float, y: Float) { + content_text_view.selectEndMove(x, y - headerHeight) + } + + fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { + content_text_view.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + + fun cancelSelect() { + content_text_view.cancelSelect() + } + + val selectedText: String get() = content_text_view.selectedText + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/DataSource.kt b/app/src/main/java/com/novel/read/ui/read/page/DataSource.kt new file mode 100644 index 0000000..4ca04fe --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/DataSource.kt @@ -0,0 +1,21 @@ +package com.novel.read.ui.read.page + +import com.novel.read.data.read.TextChapter +import com.novel.read.service.help.ReadBook + +interface DataSource { + + val pageIndex: Int get() = ReadBook.durChapterPos() + + val currentChapter: TextChapter? + + val nextChapter: TextChapter? + + val prevChapter: TextChapter? + + fun hasNextChapter(): Boolean + + fun hasPrevChapter(): Boolean + + fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/PageFactory.kt b/app/src/main/java/com/novel/read/ui/read/page/PageFactory.kt new file mode 100644 index 0000000..1ba2246 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/PageFactory.kt @@ -0,0 +1,26 @@ +package com.novel.read.ui.read.page + +abstract class PageFactory(protected val dataSource: DataSource) { + + abstract fun moveToFirst() + + abstract fun moveToLast() + + abstract fun moveToNext(upContent: Boolean): Boolean + + abstract fun moveToPrev(upContent: Boolean): Boolean + + abstract val nextPage: DATA + + abstract val prevPage: DATA + + abstract val currentPage: DATA + + abstract val nextPagePlus: DATA + + abstract fun hasNext(): Boolean + + abstract fun hasPrev(): Boolean + + abstract fun hasNextPlus(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/PageView.kt b/app/src/main/java/com/novel/read/ui/read/page/PageView.kt new file mode 100644 index 0000000..4902008 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/PageView.kt @@ -0,0 +1,416 @@ +package com.novel.read.ui.read.page + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.widget.FrameLayout +import com.novel.read.data.read.TextChapter +import com.novel.read.help.ReadBookConfig +import com.novel.read.service.help.ReadBook +import com.novel.read.ui.read.page.delegate.* +import com.novel.read.utils.ext.accentColor +import com.novel.read.utils.ext.activity +import com.novel.read.utils.ext.screenshot + +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import kotlinx.android.synthetic.main.activity_read_book.view.* +import kotlin.math.abs + +class PageView(context: Context, attrs: AttributeSet) : + FrameLayout(context, attrs), + DataSource { + + val callBack: CallBack get() = activity as CallBack + var pageFactory: TextPageFactory = TextPageFactory(this) + var pageDelegate: PageDelegate? = null + private set(value) { + field?.onDestroy() + field = null + field = value + upContent() + } + var isScroll = ReadBook.pageAnim() == 3 + var prevPage: ContentView = ContentView(context) + var curPage: ContentView = ContentView(context) + var nextPage: ContentView = ContentView(context) + val defaultAnimationSpeed = 300 + private var pressDown = false + private var isMove = false + + //起始点 + var startX: Float = 0f + var startY: Float = 0f + + //上一个触碰点 + var lastX: Float = 0f + var lastY: Float = 0f + + //触碰点 + var touchX: Float = 0f + var touchY: Float = 0f + + //是否停止动画动作 + var isAbortAnim = false + + //长按 + private var longPressed = false + private val longPressTimeout = 600L + private val longPressRunnable = Runnable { + longPressed = true + onLongPress() + } + var isTextSelected = false + private var pressOnTextSelected = false + private var firstRelativePage = 0 + private var firstLineIndex: Int = 0 + private var firstCharIndex: Int = 0 + + val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop } + private val centerRectF = RectF(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) + private val autoPageRect by lazy { Rect() } + private val autoPagePint by lazy { + Paint().apply { + color = context.accentColor + } + } + + init { + addView(nextPage) + addView(curPage) + addView(prevPage) + upBg() + setWillNotDraw(false) + upPageAnim() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + centerRectF.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) + prevPage.x = -w.toFloat() + pageDelegate?.setViewSize(w, h) + if (oldw != 0 && oldh != 0) { + ReadBook.loadContent(resetPageOffset = false) + } + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + pageDelegate?.onDraw(canvas) + if (callBack.isAutoPage) { + nextPage.screenshot()?.let { + val bottom = + page_view.height * callBack.autoPageProgress / (ReadBookConfig.autoReadSpeed * 10) + autoPageRect.set(0, 0, page_view.width, bottom) + canvas.drawBitmap(it, autoPageRect, autoPageRect, null) + canvas.drawRect( + 0f, + bottom.toFloat() - 1, + page_view.width.toFloat(), + bottom.toFloat(), + autoPagePint + ) + } + } + } + + override fun computeScroll() { + pageDelegate?.scroll() + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } + + /** + * 触摸事件 + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + callBack.screenOffTimerStart() + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (isTextSelected) { + curPage.cancelSelect() + isTextSelected = false + pressOnTextSelected = true + } else { + pressOnTextSelected = false + } + longPressed = false + postDelayed(longPressRunnable, longPressTimeout) + pressDown = true + isMove = false + pageDelegate?.onTouch(event) + pageDelegate?.onDown() + setStartPoint(event.x, event.y) + } + MotionEvent.ACTION_MOVE -> { + pressDown = true + if (!isMove) { + isMove = + abs(startX - event.x) > slopSquare || abs(startY - event.y) > slopSquare + } + if (isMove) { + longPressed = false + removeCallbacks(longPressRunnable) + if (isTextSelected) { + selectText(event.x, event.y) + } else { + pageDelegate?.onTouch(event) + } + } + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + removeCallbacks(longPressRunnable) + if (!pressDown) return true + if (!isMove) { + if (!longPressed && !pressOnTextSelected) { + onSingleTapUp() + return true + } + } + if (isTextSelected) { + callBack.showTextActionMenu() + } else if (isMove) { + pageDelegate?.onTouch(event) + } + pressOnTextSelected = false + } + } + return true + } + + fun upStatusBar() { + curPage.upStatusBar() + prevPage.upStatusBar() + nextPage.upStatusBar() + } + + /** + * 保存开始位置 + */ + fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) { + startX = x + startY = y + lastX = x + lastY = y + touchX = x + touchY = y + + if (invalidate) { + invalidate() + } + } + + /** + * 保存当前位置 + */ + fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { + lastX = touchX + lastY = touchY + touchX = x + touchY = y + if (invalidate) { + invalidate() + } + pageDelegate?.onScroll() + } + + /** + * 长按选择 + */ + private fun onLongPress() { + curPage.selectText(startX, startY) { relativePage, lineIndex, charIndex -> + isTextSelected = true + firstRelativePage = relativePage + firstLineIndex = lineIndex + firstCharIndex = charIndex + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + } + } + + /** + * 单击 + */ + private fun onSingleTapUp(): Boolean { + if (isTextSelected) { + isTextSelected = false + return true + } + if (centerRectF.contains(startX, startY)) { + if (!isAbortAnim) { + callBack.clickCenter() + } + } else if (ReadBookConfig.clickTurnPage) { + if (startX > width / 2 || ReadBookConfig.clickAllNext) { + pageDelegate?.nextPageByAnim(defaultAnimationSpeed) + } else { + pageDelegate?.prevPageByAnim(defaultAnimationSpeed) + } + } + return true + } + + /** + * 选择文本 + */ + private fun selectText(x: Float, y: Float) { + curPage.selectText(x, y) { relativePage, lineIndex, charIndex -> + when { + relativePage > firstRelativePage -> { + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + relativePage < firstRelativePage -> { + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + lineIndex > firstLineIndex -> { + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + lineIndex < firstLineIndex -> { + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + charIndex > firstCharIndex -> { + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + else -> { + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + } + } + } + + fun onDestroy() { + pageDelegate?.onDestroy() + curPage.cancelSelect() + } + + fun fillPage(direction: PageDelegate.Direction) { + when (direction) { + PageDelegate.Direction.PREV -> { + pageFactory.moveToPrev(true) + } + PageDelegate.Direction.NEXT -> { + pageFactory.moveToNext(true) + } + else -> Unit + } + } + + fun upPageAnim() { + isScroll = ReadBook.pageAnim() == 3 + when (ReadBook.pageAnim()) { + 0 -> if (pageDelegate !is CoverPageDelegate) { + pageDelegate = CoverPageDelegate(this) + } + 1 -> if (pageDelegate !is SlidePageDelegate) { + pageDelegate = SlidePageDelegate(this) + } + 2 -> if (pageDelegate !is SimulationPageDelegate) { + pageDelegate = SimulationPageDelegate(this) + } + 3 -> if (pageDelegate !is ScrollPageDelegate) { + pageDelegate = ScrollPageDelegate(this) + } + else -> if (pageDelegate !is NoAnimPageDelegate) { + pageDelegate = NoAnimPageDelegate(this) + } + } + } + + override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { + if (isScroll && !callBack.isAutoPage) { + curPage.setContent(pageFactory.currentPage, resetPageOffset) + } else { + curPage.resetPageOffset() + when (relativePosition) { + -1 -> prevPage.setContent(pageFactory.prevPage) + 1 -> nextPage.setContent(pageFactory.nextPage) + else -> { + curPage.setContent(pageFactory.currentPage) + nextPage.setContent(pageFactory.nextPage) + prevPage.setContent(pageFactory.prevPage) + } + } + } + callBack.screenOffTimerStart() + } + + fun upTipStyle() { + curPage.upTipStyle() + prevPage.upTipStyle() + nextPage.upTipStyle() + } + + fun upStyle() { + ChapterProvider.upStyle() + curPage.upStyle() + prevPage.upStyle() + nextPage.upStyle() + } + + fun upBg() { + ReadBookConfig.bg ?: let { + ReadBookConfig.upBg() + } + curPage.setBg(ReadBookConfig.bg) + prevPage.setBg(ReadBookConfig.bg) + nextPage.setBg(ReadBookConfig.bg) + } + + fun upTime() { + curPage.upTime() + prevPage.upTime() + nextPage.upTime() + } + + fun upBattery(battery: Int) { + curPage.upBattery(battery) + prevPage.upBattery(battery) + nextPage.upBattery(battery) + } + + override val currentChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(0) else null + } + + override val nextChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(1) else null + } + + override val prevChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null + } + + override fun hasNextChapter(): Boolean { + return ReadBook.durChapterIndex < ReadBook.chapterSize - 1 + } + + override fun hasPrevChapter(): Boolean { + return ReadBook.durChapterIndex > 0 + } + + interface CallBack { + val isInitFinish: Boolean + val isAutoPage: Boolean + val autoPageProgress: Int + fun clickCenter() + fun screenOffTimerStart() + fun showTextActionMenu() + } +} diff --git a/app/src/main/java/com/novel/read/ui/read/page/TextPageFactory.kt b/app/src/main/java/com/novel/read/ui/read/page/TextPageFactory.kt new file mode 100644 index 0000000..8482fab --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/TextPageFactory.kt @@ -0,0 +1,131 @@ +package com.novel.read.ui.read.page + +import com.novel.read.data.read.TextPage +import com.novel.read.service.help.ReadBook + + +class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource) { + + override fun hasPrev(): Boolean = with(dataSource) { + return hasPrevChapter() || pageIndex > 0 + } + + override fun hasNext(): Boolean = with(dataSource) { + return hasNextChapter() || currentChapter?.isLastIndex(pageIndex) != true + } + + override fun hasNextPlus(): Boolean = with(dataSource) { + return hasNextChapter() || pageIndex < (currentChapter?.pageSize ?: 1) - 2 + } + + override fun moveToFirst() { + ReadBook.setPageIndex(0) + } + + override fun moveToLast() = with(dataSource) { + currentChapter?.let { + if (it.pageSize == 0) { + ReadBook.setPageIndex(0) + } else { + ReadBook.setPageIndex(it.pageSize.minus(1)) + } + } ?: ReadBook.setPageIndex(0) + } + + override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) { + return if (hasNext()) { + if (currentChapter?.isLastIndex(pageIndex) == true) { + ReadBook.moveToNextChapter(upContent) + } else { + ReadBook.setPageIndex(pageIndex.plus(1)) + } + if (upContent) upContent(resetPageOffset = false) + true + } else + false + } + + override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) { + return if (hasPrev()) { + if (pageIndex <= 0) { + ReadBook.moveToPrevChapter(upContent) + } else { + ReadBook.setPageIndex(pageIndex.minus(1)) + } + if (upContent) upContent(resetPageOffset = false) + true + } else + false + } + + override val currentPage: TextPage + get() = with(dataSource) { + ReadBook.msg?.let { + return@with TextPage(text = it).format() + } + currentChapter?.let { + return@with it.page(pageIndex) + ?: TextPage(title = it.title).format() + } + return TextPage().format() + } + + override val nextPage: TextPage + get() = with(dataSource) { + ReadBook.msg?.let { + return@with TextPage(text = it).format() + } + currentChapter?.let { + if (pageIndex < it.pageSize - 1) { + return@with it.page(pageIndex + 1)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + } + if (!hasNextChapter()) { + return@with TextPage(text = "") + } + nextChapter?.let { + return@with it.page(0)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + return TextPage().format() + } + + override val prevPage: TextPage + get() = with(dataSource) { + ReadBook.msg?.let { + return@with TextPage(text = it).format() + } + if (pageIndex > 0) { + currentChapter?.let { + return@with it.page(pageIndex - 1)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + } + prevChapter?.let { + return@with it.lastPage?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + return TextPage().format() + } + + override val nextPagePlus: TextPage + get() = with(dataSource) { + currentChapter?.let { + if (pageIndex < it.pageSize - 2) { + return@with it.page(pageIndex + 2)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + nextChapter?.let { nc -> + if (pageIndex < it.pageSize - 1) { + return@with nc.page(0)?.removePageAloudSpan() + ?: TextPage(title = nc.title).format() + } + return@with nc.page(1)?.removePageAloudSpan() + ?: TextPage(title = nc.title).format() + } + + } + return TextPage().format() + } +} diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/CoverPageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/CoverPageDelegate.kt new file mode 100644 index 0000000..69ec526 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/CoverPageDelegate.kt @@ -0,0 +1,83 @@ +package com.novel.read.ui.read.page.delegate + +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.drawable.GradientDrawable +import com.novel.read.ui.read.page.PageView + +class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { + private val bitmapMatrix = Matrix() + private val shadowDrawableR: GradientDrawable + + init { + val shadowColors = intArrayOf(0x66111111, 0x00000000) + shadowDrawableR = GradientDrawable( + GradientDrawable.Orientation.LEFT_RIGHT, shadowColors + ) + shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT + } + + override fun onDraw(canvas: Canvas) { + if (!isRunning) return + val offsetX = touchX - startX + + if ((mDirection == Direction.NEXT && offsetX > 0) + || (mDirection == Direction.PREV && offsetX < 0) + ) { + return + } + + val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth + if (mDirection == Direction.PREV) { + bitmapMatrix.setTranslate(distanceX, 0.toFloat()) + curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } + prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } + addShadow(distanceX.toInt(), canvas) + } else if (mDirection == Direction.NEXT) { + bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) + nextBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } + curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } + addShadow(distanceX.toInt(), canvas) + } + } + + private fun addShadow(left: Int, canvas: Canvas) { + if (left < 0) { + shadowDrawableR.setBounds(left + viewWidth, 0, left + viewWidth + 30, viewHeight) + shadowDrawableR.draw(canvas) + } else if (left > 0) { + shadowDrawableR.setBounds(left, 0, left + 30, viewHeight) + shadowDrawableR.draw(canvas) + } + } + + override fun onAnimStop() { + if (!isCancel) { + pageView.fillPage(mDirection) + } + } + + override fun onAnimStart(animationSpeed: Int) { + val distanceX: Float + when (mDirection) { + Direction.NEXT -> distanceX = + if (isCancel) { + var dis = viewWidth - startX + touchX + if (dis > viewWidth) { + dis = viewWidth.toFloat() + } + viewWidth - dis + } else { + -(touchX + (viewWidth - startX)) + } + else -> distanceX = + if (isCancel) { + -(touchX - startX) + } else { + viewWidth - (touchX - startX) + } + } + startScroll(touchX.toInt(), 0, distanceX.toInt(), 0, animationSpeed) + } + +} diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/HorizontalPageDelegate.kt new file mode 100644 index 0000000..54e290a --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/HorizontalPageDelegate.kt @@ -0,0 +1,143 @@ +package com.novel.read.ui.read.page.delegate + +import android.graphics.Bitmap +import android.view.MotionEvent +import com.novel.read.utils.ext.screenshot +import com.novel.read.ui.read.page.PageView + +abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) { + + protected var curBitmap: Bitmap? = null + protected var prevBitmap: Bitmap? = null + protected var nextBitmap: Bitmap? = null + + override fun setDirection(direction: Direction) { + super.setDirection(direction) + setBitmap() + } + + private fun setBitmap() { + when (mDirection) { + Direction.PREV -> { + prevBitmap?.recycle() + prevBitmap = prevPage.screenshot() + curBitmap?.recycle() + curBitmap = curPage.screenshot() + } + Direction.NEXT -> { + nextBitmap?.recycle() + nextBitmap = nextPage.screenshot() + curBitmap?.recycle() + curBitmap = curPage.screenshot() + } + else -> Unit + } + } + + override fun onTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + abortAnim() + } + MotionEvent.ACTION_MOVE -> { + onScroll(event) + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + onAnimStart(pageView.defaultAnimationSpeed) + } + } + } + + private fun onScroll(event: MotionEvent) { + + val action: Int = event.action + val pointerUp = + action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UP + val skipIndex = if (pointerUp) event.actionIndex else -1 + // Determine focal point + var sumX = 0f + var sumY = 0f + val count: Int = event.pointerCount + for (i in 0 until count) { + if (skipIndex == i) continue + sumX += event.getX(i) + sumY += event.getY(i) + } + val div = if (pointerUp) count - 1 else count + val focusX = sumX / div + val focusY = sumY / div + //判断是否移动了 + if (!isMoved) { + val deltaX = (focusX - startX).toInt() + val deltaY = (focusY - startY).toInt() + val distance = deltaX * deltaX + deltaY * deltaY + isMoved = distance > pageView.slopSquare + if (isMoved) { + if (sumX - startX > 0) { + //如果上一页不存在 + if (!hasPrev()) { + noNext = true + return + } + setDirection(Direction.PREV) + } else { + //如果不存在表示没有下一页了 + if (!hasNext()) { + noNext = true + return + } + setDirection(Direction.NEXT) + } + } + } + if (isMoved) { + isCancel = if (mDirection == Direction.NEXT) sumX > lastX else sumX < lastX + isRunning = true + //设置触摸点 + pageView.setTouchPoint(sumX, sumY) + } + } + + override fun abortAnim() { + isStarted = false + isMoved = false + isRunning = false + if (!scroller.isFinished) { + pageView.isAbortAnim = true + scroller.abortAnimation() + if (!isCancel) { + pageView.fillPage(mDirection) + pageView.invalidate() + } + } else { + pageView.isAbortAnim = false + } + } + + override fun nextPageByAnim(animationSpeed: Int) { + abortAnim() + if (!hasNext()) return + setDirection(Direction.NEXT) + pageView.setTouchPoint(viewWidth.toFloat(), 0f, false) + onAnimStart(animationSpeed) + } + + override fun prevPageByAnim(animationSpeed: Int) { + abortAnim() + if (!hasPrev()) return + setDirection(Direction.PREV) + pageView.setTouchPoint(0f, 0f) + onAnimStart(animationSpeed) + } + + override fun onDestroy() { + super.onDestroy() + prevBitmap?.recycle() + prevBitmap = null + curBitmap?.recycle() + curBitmap = null + nextBitmap?.recycle() + nextBitmap = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/NoAnimPageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/NoAnimPageDelegate.kt new file mode 100644 index 0000000..80be268 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/NoAnimPageDelegate.kt @@ -0,0 +1,24 @@ +package com.novel.read.ui.read.page.delegate + +import android.graphics.Canvas +import com.novel.read.ui.read.page.PageView + +class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { + + override fun onAnimStart(animationSpeed: Int) { + if (!isCancel) { + pageView.fillPage(mDirection) + } + stopScroll() + } + + override fun onDraw(canvas: Canvas) { + // nothing + } + + override fun onAnimStop() { + // nothing + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/PageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/PageDelegate.kt new file mode 100644 index 0000000..64edeee --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/PageDelegate.kt @@ -0,0 +1,191 @@ +package com.novel.read.ui.read.page.delegate + +import android.content.Context +import android.graphics.Canvas +import android.view.MotionEvent +import android.view.animation.DecelerateInterpolator +import android.widget.Scroller +import androidx.annotation.CallSuper +import com.google.android.material.snackbar.Snackbar +import com.novel.read.R +import com.novel.read.ui.read.page.ContentView +import com.novel.read.ui.read.page.PageView +import kotlin.math.abs + +abstract class PageDelegate(protected val pageView: PageView) { + + protected val context: Context = pageView.context + + //起始点 + protected val startX: Float get() = pageView.startX + protected val startY: Float get() = pageView.startY + + //上一个触碰点 + protected val lastX: Float get() = pageView.lastX + protected val lastY: Float get() = pageView.lastY + + //触碰点 + protected val touchX: Float get() = pageView.touchX + protected val touchY: Float get() = pageView.touchY + + protected val nextPage: ContentView get() = pageView.nextPage + protected val curPage: ContentView get() = pageView.curPage + protected val prevPage: ContentView get() = pageView.prevPage + + protected var viewWidth: Int = pageView.width + protected var viewHeight: Int = pageView.height + + protected val scroller: Scroller by lazy { + Scroller(pageView.context, DecelerateInterpolator()) + } + + private val snackBar: Snackbar by lazy { + Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT) + } + + var isMoved = false + var noNext = true + + //移动方向 + var mDirection = Direction.NONE + var isCancel = false + var isRunning = false + var isStarted = false + + private var selectedOnDown = false + + init { + curPage.resetPageOffset() + } + + open fun fling( + startX: Int, startY: Int, velocityX: Int, velocityY: Int, + minX: Int, maxX: Int, minY: Int, maxY: Int + ) { + scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) + isRunning = true + isStarted = true + pageView.invalidate() + } + + protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, animationSpeed: Int) { + val duration = if (dx != 0) { + (animationSpeed * abs(dx)) / viewWidth + } else { + (animationSpeed * abs(dy)) / viewHeight + } + scroller.startScroll(startX, startY, dx, dy, duration) + isRunning = true + isStarted = true + pageView.invalidate() + } + + protected fun stopScroll() { + isStarted = false + pageView.post { + isMoved = false + isRunning = false + pageView.invalidate() + } + } + + open fun setViewSize(width: Int, height: Int) { + viewWidth = width + viewHeight = height + } + + fun scroll() { + if (scroller.computeScrollOffset()) { + pageView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) + } else if (isStarted) { + onAnimStop() + stopScroll() + } + } + + open fun onScroll() = Unit + + abstract fun abortAnim() + + abstract fun onAnimStart(animationSpeed: Int) //scroller start + + abstract fun onDraw(canvas: Canvas) //绘制 + + abstract fun onAnimStop() //scroller finish + + abstract fun nextPageByAnim(animationSpeed: Int) + + abstract fun prevPageByAnim(animationSpeed: Int) + + open fun keyTurnPage(direction: Direction) { + if (isRunning) return + when (direction) { + Direction.NEXT -> nextPageByAnim(100) + Direction.PREV -> prevPageByAnim(100) + else -> return + } + } + + @CallSuper + open fun setDirection(direction: Direction) { + mDirection = direction + } + + /** + * 触摸事件处理 + */ + abstract fun onTouch(event: MotionEvent) + + /** + * 按下 + */ + fun onDown() { + //是否移动 + isMoved = false + //是否存在下一章 + noNext = false + //是否正在执行动画 + isRunning = false + //取消 + isCancel = false + //是下一章还是前一章 + setDirection(Direction.NONE) + } + + /** + * 判断是否有上一页 + */ + fun hasPrev(): Boolean { + val hasPrev = pageView.pageFactory.hasPrev() + if (!hasPrev) { + if (!snackBar.isShown) { + snackBar.setText(R.string.no_prev_page) + snackBar.show() + } + } + return hasPrev + } + + /** + * 判断是否有下一页 + */ + fun hasNext(): Boolean { + val hasNext = pageView.pageFactory.hasNext() + if (!hasNext) { + if (!snackBar.isShown) { + snackBar.setText(R.string.no_next_page) + snackBar.show() + } + } + return hasNext + } + + open fun onDestroy() { + + } + + enum class Direction { + NONE, PREV, NEXT + } + +} diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/ScrollPageDelegate.kt new file mode 100644 index 0000000..f21ca90 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/ScrollPageDelegate.kt @@ -0,0 +1,114 @@ +package com.novel.read.ui.read.page.delegate + +import android.graphics.Canvas +import android.view.MotionEvent +import android.view.VelocityTracker +import com.novel.read.ui.read.page.PageView +import io.legado.app.ui.book.read.page.provider.ChapterProvider + +class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { + + // 滑动追踪的时间 + private val velocityDuration = 1000 + //速度追踪器 + private val mVelocity: VelocityTracker = VelocityTracker.obtain() + + override fun onAnimStart(animationSpeed: Int) { + //惯性滚动 + fling( + 0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(), + 0, 0, -10 * viewHeight, 10 * viewHeight + ) + } + + override fun onAnimStop() { + // nothing + } + + override fun onTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + abortAnim() + mVelocity.clear() + } + MotionEvent.ACTION_MOVE -> { + onScroll(event) + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + onAnimStart(pageView.defaultAnimationSpeed) + } + } + } + + override fun onScroll() { + curPage.onScroll(touchY - lastY) + } + + override fun onDraw(canvas: Canvas) { + // nothing + } + + private fun onScroll(event: MotionEvent) { + mVelocity.addMovement(event) + mVelocity.computeCurrentVelocity(velocityDuration) + val action: Int = event.action + val pointerUp = + action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UP + val skipIndex = if (pointerUp) event.actionIndex else -1 + // Determine focal point + var sumX = 0f + var sumY = 0f + val count: Int = event.pointerCount + for (i in 0 until count) { + if (skipIndex == i) continue + sumX += event.getX(i) + sumY += event.getY(i) + } + val div = if (pointerUp) count - 1 else count + val focusX = sumX / div + val focusY = sumY / div + pageView.setTouchPoint(sumX, sumY) + if (!isMoved) { + val deltaX = (focusX - startX).toInt() + val deltaY = (focusY - startY).toInt() + val distance = deltaX * deltaX + deltaY * deltaY + isMoved = distance > pageView.slopSquare + } + if (isMoved) { + isRunning = true + } + } + + override fun onDestroy() { + super.onDestroy() + mVelocity.recycle() + } + + override fun abortAnim() { + isStarted = false + isMoved = false + isRunning = false + if (!scroller.isFinished) { + pageView.isAbortAnim = true + scroller.abortAnimation() + } else { + pageView.isAbortAnim = false + } + } + + override fun nextPageByAnim(animationSpeed: Int) { + if (pageView.isAbortAnim) { + return + } + pageView.setStartPoint(0f, 0f, false) + startScroll(0, 0, 0, -ChapterProvider.visibleHeight, animationSpeed) + } + + override fun prevPageByAnim(animationSpeed: Int) { + if (pageView.isAbortAnim) { + return + } + pageView.setStartPoint(0f, 0f, false) + startScroll(0, 0, 0, ChapterProvider.visibleHeight, animationSpeed) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/SimulationPageDelegate.kt new file mode 100644 index 0000000..056644f --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/SimulationPageDelegate.kt @@ -0,0 +1,561 @@ +package com.novel.read.ui.read.page.delegate + +import android.graphics.* +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.view.MotionEvent +import com.novel.read.help.ReadBookConfig +import com.novel.read.ui.read.page.PageView +import kotlin.math.* + +@Suppress("DEPRECATION") +class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { + //不让x,y为0,否则在点计算时会有问题 + private var mTouchX = 0.1f + private var mTouchY = 0.1f + // 拖拽点对应的页脚 + private var mCornerX = 1 + private var mCornerY = 1 + private val mPath0: Path = Path() + private val mPath1: Path = Path() + // 贝塞尔曲线起始点 + private val mBezierStart1 = PointF() + // 贝塞尔曲线控制点 + private val mBezierControl1 = PointF() + // 贝塞尔曲线顶点 + private val mBezierVertex1 = PointF() + // 贝塞尔曲线结束点 + private var mBezierEnd1 = PointF() + + // 另一条贝塞尔曲线 + // 贝塞尔曲线起始点 + private val mBezierStart2 = PointF() + // 贝塞尔曲线控制点 + private val mBezierControl2 = PointF() + // 贝塞尔曲线顶点 + private val mBezierVertex2 = PointF() + // 贝塞尔曲线结束点 + private var mBezierEnd2 = PointF() + + private var mMiddleX = 0f + private var mMiddleY = 0f + private var mDegrees = 0f + private var mTouchToCornerDis = 0f + private var mColorMatrixFilter = ColorMatrixColorFilter( + ColorMatrix( + floatArrayOf( + 1f, 0f, 0f, 0f, 0f, + 0f, 1f, 0f, 0f, 0f, + 0f, 0f, 1f, 0f, 0f, + 0f, 0f, 0f, 1f, 0f + ) + ) + ) + private val mMatrix: Matrix = Matrix() + private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f) + + // 是否属于右上左下 + private var mIsRtOrLb = false + private var mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat() + // 背面颜色组 + private var mBackShadowColors: IntArray + // 前面颜色组 + private var mFrontShadowColors: IntArray + // 有阴影的GradientDrawable + private var mBackShadowDrawableLR: GradientDrawable + private var mBackShadowDrawableRL: GradientDrawable + private var mFolderShadowDrawableLR: GradientDrawable + private var mFolderShadowDrawableRL: GradientDrawable + + private var mFrontShadowDrawableHBT: GradientDrawable + private var mFrontShadowDrawableHTB: GradientDrawable + private var mFrontShadowDrawableVLR: GradientDrawable + private var mFrontShadowDrawableVRL: GradientDrawable + + private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL } + + init { + //设置颜色数组 + val color = intArrayOf(0x333333, -0x4fcccccd) + mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color) + mFolderShadowDrawableRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFolderShadowDrawableLR = GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, color) + mFolderShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) + mBackShadowDrawableRL = + GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors) + mBackShadowDrawableRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mBackShadowDrawableLR = + GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors) + mBackShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) + mFrontShadowDrawableVLR = + GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors) + mFrontShadowDrawableVLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableVRL = + GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors) + mFrontShadowDrawableVRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableHTB = + GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors) + mFrontShadowDrawableHTB.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableHBT = + GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors) + mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT + } + + override fun setViewSize(width: Int, height: Int) { + super.setViewSize(width, height) + mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat() + } + + override fun onTouch(event: MotionEvent) { + super.onTouch(event) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + calcCornerXY(event.x, event.y) + } + MotionEvent.ACTION_MOVE -> { + if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3) + || mDirection == Direction.PREV + ) { + pageView.touchY = viewHeight.toFloat() + } + + if (startY > viewHeight / 3 && startY < viewHeight / 2 + && mDirection == Direction.NEXT + ) { + pageView.touchY = 1f + } + } + } + } + + override fun setDirection(direction: Direction) { + super.setDirection(direction) + when (direction) { + Direction.PREV -> + //上一页滑动不出现对角 + if (startX > viewWidth / 2) { + calcCornerXY(startX, viewHeight.toFloat()) + } else { + calcCornerXY(viewWidth - startX, viewHeight.toFloat()) + } + Direction.NEXT -> + if (viewWidth / 2 > startX) { + calcCornerXY(viewWidth - startX, startY) + } + else -> Unit + } + } + + override fun onAnimStart(animationSpeed: Int) { + var dx: Float + val dy: Float + // dy 垂直方向滑动的距离,负值会使滚动向上滚动 + if (isCancel) { + dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + (viewWidth - touchX) + } else { + -touchX + } + if (mDirection != Direction.NEXT) { + dx = -(viewWidth + touchX) + } + dy = if (mCornerY > 0) { + (viewHeight - touchY) + } else { + -touchY // 防止mTouchY最终变为0 + } + } else { + dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + -(viewWidth + touchX) + } else { + (viewWidth - touchX + viewWidth) + } + dy = if (mCornerY > 0) { + (viewHeight - touchY) + } else { + (1 - touchY) // 防止mTouchY最终变为0 + } + } + startScroll(touchX.toInt(), touchY.toInt(), dx.toInt(), dy.toInt(), animationSpeed) + } + + override fun onAnimStop() { + if (!isCancel) { + pageView.fillPage(mDirection) + } + } + + override fun onDraw(canvas: Canvas) { + if (!isRunning) return + when (mDirection) { + Direction.NEXT -> { + calcPoints() + drawCurrentPageArea(canvas, curBitmap) + drawNextPageAreaAndShadow(canvas, nextBitmap) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, curBitmap) + } + Direction.PREV -> { + calcPoints() + drawCurrentPageArea(canvas, prevBitmap) + drawNextPageAreaAndShadow(canvas, curBitmap) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, prevBitmap) + } + else -> return + } + } + + /** + * 绘制翻起页背面 + */ + private fun drawCurrentBackArea( + canvas: Canvas, + bitmap: Bitmap? + ) { + bitmap ?: return + val i = ((mBezierStart1.x + mBezierControl1.x) / 2).toInt() + val f1 = abs(i - mBezierControl1.x) + val i1 = ((mBezierStart2.y + mBezierControl2.y) / 2).toInt() + val f2 = abs(i1 - mBezierControl2.y) + val f3 = min(f1, f2) + mPath1.reset() + mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y) + mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) + mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y) + mPath1.lineTo(mTouchX, mTouchY) + mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y) + mPath1.close() + val mFolderShadowDrawable: GradientDrawable + val left: Int + val right: Int + if (mIsRtOrLb) { + left = (mBezierStart1.x - 1).toInt() + right = (mBezierStart1.x + f3 + 1).toInt() + mFolderShadowDrawable = mFolderShadowDrawableLR + } else { + left = (mBezierStart1.x - f3 - 1).toInt() + right = (mBezierStart1.x + 1).toInt() + mFolderShadowDrawable = mFolderShadowDrawableRL + } + canvas.save() + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) + } else { + canvas.clipPath(mPath1, Region.Op.INTERSECT) + } + + mPaint.colorFilter = mColorMatrixFilter + val dis = hypot( + mCornerX - mBezierControl1.x.toDouble(), + mBezierControl2.y - mCornerY.toDouble() + ).toFloat() + val f8 = (mCornerX - mBezierControl1.x) / dis + val f9 = (mBezierControl2.y - mCornerY) / dis + mMatrixArray[0] = 1 - 2 * f9 * f9 + mMatrixArray[1] = 2 * f8 * f9 + mMatrixArray[3] = mMatrixArray[1] + mMatrixArray[4] = 1 - 2 * f8 * f8 + mMatrix.reset() + mMatrix.setValues(mMatrixArray) + mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y) + mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y) + canvas.drawColor(ReadBookConfig.bgMeanColor) + canvas.drawBitmap(bitmap, mMatrix, mPaint) + mPaint.colorFilter = null + canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) + mFolderShadowDrawable.setBounds( + left, mBezierStart1.y.toInt(), + right, (mBezierStart1.y + mMaxLength).toInt() + ) + mFolderShadowDrawable.draw(canvas) + canvas.restore() + } + + /** + * 绘制翻起页的阴影 + */ + private fun drawCurrentPageShadow(canvas: Canvas) { + val degree: Double = if (mIsRtOrLb) { + Math.PI / 4 - atan2(mBezierControl1.y - mTouchY, mTouchX - mBezierControl1.x) + } else { + Math.PI / 4 - atan2(mTouchY - mBezierControl1.y, mTouchX - mBezierControl1.x) + } + // 翻起页阴影顶点与touch点的距离 + val d1 = 25.toFloat() * 1.414 * cos(degree) + val d2 = 25.toFloat() * 1.414 * sin(degree) + val x = (mTouchX + d1).toFloat() + val y: Float = if (mIsRtOrLb) { + (mTouchY + d2).toFloat() + } else { + (mTouchY - d2).toFloat() + } + mPath1.reset() + mPath1.moveTo(x, y) + mPath1.lineTo(mTouchX, mTouchY) + mPath1.lineTo(mBezierControl1.x, mBezierControl1.y) + mPath1.lineTo(mBezierStart1.x, mBezierStart1.y) + mPath1.close() + canvas.save() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) + } + canvas.clipPath(mPath1, Region.Op.INTERSECT) + + var leftX: Int + var rightX: Int + var mCurrentPageShadow: GradientDrawable + if (mIsRtOrLb) { + leftX = mBezierControl1.x.toInt() + rightX = (mBezierControl1.x + 25).toInt() + mCurrentPageShadow = mFrontShadowDrawableVLR + } else { + leftX = (mBezierControl1.x - 25).toInt() + rightX = (mBezierControl1.x + 1).toInt() + mCurrentPageShadow = mFrontShadowDrawableVRL + } + var rotateDegrees = Math.toDegrees( + atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble() + ).toFloat() + canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) + mCurrentPageShadow.setBounds( + leftX, (mBezierControl1.y - mMaxLength).toInt(), + rightX, mBezierControl1.y.toInt() + ) + mCurrentPageShadow.draw(canvas) + canvas.restore() + + mPath1.reset() + mPath1.moveTo(x, y) + mPath1.lineTo(mTouchX, mTouchY) + mPath1.lineTo(mBezierControl2.x, mBezierControl2.y) + mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) + mPath1.close() + canvas.save() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) + } + canvas.clipPath(mPath1) + + if (mIsRtOrLb) { + leftX = mBezierControl2.y.toInt() + rightX = (mBezierControl2.y + 25).toInt() + mCurrentPageShadow = mFrontShadowDrawableHTB + } else { + leftX = (mBezierControl2.y - 25).toInt() + rightX = (mBezierControl2.y + 1).toInt() + mCurrentPageShadow = mFrontShadowDrawableHBT + } + rotateDegrees = Math.toDegrees( + atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble() + ).toFloat() + canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) + val temp = + if (mBezierControl2.y < 0) (mBezierControl2.y - viewHeight).toDouble() + else mBezierControl2.y.toDouble() + val hmg = hypot(mBezierControl2.x.toDouble(), temp) + if (hmg > mMaxLength) + mCurrentPageShadow.setBounds( + (mBezierControl2.x - 25 - hmg).toInt(), leftX, + (mBezierControl2.x + mMaxLength - hmg).toInt(), rightX + ) + else + mCurrentPageShadow.setBounds( + (mBezierControl2.x - mMaxLength).toInt(), leftX, + mBezierControl2.x.toInt(), rightX + ) + mCurrentPageShadow.draw(canvas) + canvas.restore() + } + + // + private fun drawNextPageAreaAndShadow( + canvas: Canvas, + bitmap: Bitmap? + ) { + bitmap ?: return + mPath1.reset() + mPath1.moveTo(mBezierStart1.x, mBezierStart1.y) + mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) + mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y) + mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) + mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) + mPath1.close() + mDegrees = Math.toDegrees( + atan2( + (mBezierControl1.x - mCornerX).toDouble(), + mBezierControl2.y - mCornerY.toDouble() + ) + ).toFloat() + val leftX: Int + val rightX: Int + val mBackShadowDrawable: GradientDrawable + if (mIsRtOrLb) { //左下及右上 + leftX = mBezierStart1.x.toInt() + rightX = (mBezierStart1.x + mTouchToCornerDis / 4).toInt() + mBackShadowDrawable = mBackShadowDrawableLR + } else { + leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt() + rightX = mBezierStart1.x.toInt() + mBackShadowDrawable = mBackShadowDrawableRL + } + canvas.save() + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) + } else { + canvas.clipPath(mPath1, Region.Op.INTERSECT) + } + canvas.drawBitmap(bitmap, 0f, 0f, null) + canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) + mBackShadowDrawable.setBounds( + leftX, mBezierStart1.y.toInt(), + rightX, (mMaxLength + mBezierStart1.y).toInt() + ) //左上及右下角的xy坐标值,构成一个矩形 + mBackShadowDrawable.draw(canvas) + canvas.restore() + } + + // + private fun drawCurrentPageArea( + canvas: Canvas, + bitmap: Bitmap? + ) { + bitmap ?: return + mPath0.reset() + mPath0.moveTo(mBezierStart1.x, mBezierStart1.y) + mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, mBezierEnd1.y) + mPath0.lineTo(mTouchX, mTouchY) + mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y) + mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y) + mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) + mPath0.close() + + canvas.save() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) + } + canvas.drawBitmap(bitmap, 0f, 0f, null) + canvas.restore() + } + + /** + * 计算拖拽点对应的拖拽脚 + */ + private fun calcCornerXY(x: Float, y: Float) { + mCornerX = if (x <= viewWidth / 2) 0 else viewWidth + mCornerY = if (y <= viewHeight / 2) 0 else viewHeight + mIsRtOrLb = (mCornerX == 0 && mCornerY == viewHeight) + || (mCornerY == 0 && mCornerX == viewWidth) + } + + private fun calcPoints() { + mTouchX = touchX + mTouchY = touchY + + mMiddleX = (mTouchX + mCornerX) / 2 + mMiddleY = (mTouchY + mCornerY) / 2 + mBezierControl1.x = + mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) + mBezierControl1.y = mCornerY.toFloat() + mBezierControl2.x = mCornerX.toFloat() + + val f4 = mCornerY - mMiddleY + if (f4 == 0f) { + mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f + + } else { + mBezierControl2.y = + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) + } + mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 + mBezierStart1.y = mCornerY.toFloat() + + // 固定左边上下两个点 + if (mTouchX > 0 && mTouchX < viewWidth) { + if (mBezierStart1.x < 0 || mBezierStart1.x > viewWidth) { + if (mBezierStart1.x < 0) + mBezierStart1.x = viewWidth - mBezierStart1.x + + val f1 = abs(mCornerX - mTouchX) + val f2 = viewWidth * f1 / mBezierStart1.x + mTouchX = abs(mCornerX - f2) + + val f3 = abs(mCornerX - mTouchX) * abs(mCornerY - mTouchY) / f1 + mTouchY = abs(mCornerY - f3) + + mMiddleX = (mTouchX + mCornerX) / 2 + mMiddleY = (mTouchY + mCornerY) / 2 + + mBezierControl1.x = + mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) + mBezierControl1.y = mCornerY.toFloat() + + mBezierControl2.x = mCornerX.toFloat() + + val f5 = mCornerY - mMiddleY + if (f5 == 0f) { + mBezierControl2.y = + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f + } else { + mBezierControl2.y = + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) + } + + mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 + } + } + mBezierStart2.x = mCornerX.toFloat() + mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2 + + mTouchToCornerDis = hypot( + (mTouchX - mCornerX).toDouble(), + (mTouchY - mCornerY).toDouble() + ).toFloat() + + mBezierEnd1 = getCross( + PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1, + mBezierStart2 + ) + mBezierEnd2 = getCross( + PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1, + mBezierStart2 + ) + + mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 + mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 + mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 + mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4 + } + + /** + * 求解直线P1P2和直线P3P4的交点坐标 + */ + private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF { + val crossP = PointF() + // 二元函数通式: y=ax+b + val a1 = (P2.y - P1.y) / (P2.x - P1.x) + val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x) + val a2 = (P4.y - P3.y) / (P4.x - P3.x) + val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x) + crossP.x = (b2 - b1) / (a1 - a2) + crossP.y = a1 * crossP.x + b1 + return crossP + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/page/delegate/SlidePageDelegate.kt b/app/src/main/java/com/novel/read/ui/read/page/delegate/SlidePageDelegate.kt new file mode 100644 index 0000000..3601b3b --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/page/delegate/SlidePageDelegate.kt @@ -0,0 +1,60 @@ +package com.novel.read.ui.read.page.delegate + +import android.graphics.Canvas +import android.graphics.Matrix +import com.novel.read.ui.read.page.PageView + +class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { + + private val bitmapMatrix = Matrix() + + override fun onAnimStart(animationSpeed: Int) { + val distanceX: Float + when (mDirection) { + Direction.NEXT -> distanceX = + if (isCancel) { + var dis = viewWidth - startX + touchX + if (dis > viewWidth) { + dis = viewWidth.toFloat() + } + viewWidth - dis + } else { + -(touchX + (viewWidth - startX)) + } + else -> distanceX = + if (isCancel) { + -(touchX - startX) + } else { + viewWidth - (touchX - startX) + } + } + startScroll(touchX.toInt(), 0, distanceX.toInt(), 0, animationSpeed) + } + + override fun onDraw(canvas: Canvas) { + val offsetX = touchX - startX + + if ((mDirection == Direction.NEXT && offsetX > 0) + || (mDirection == Direction.PREV && offsetX < 0) + ) return + val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth + if (!isRunning) return + if (mDirection == Direction.PREV) { + bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat()) + curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } + bitmapMatrix.setTranslate(distanceX, 0.toFloat()) + prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } + } else if (mDirection == Direction.NEXT) { + bitmapMatrix.setTranslate(distanceX, 0.toFloat()) + nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } + bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) + curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } + } + } + + override fun onAnimStop() { + if (!isCancel) { + pageView.fillPage(mDirection) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/read/provider/ChapterProvider.kt b/app/src/main/java/com/novel/read/ui/read/provider/ChapterProvider.kt new file mode 100644 index 0000000..9830ef7 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/provider/ChapterProvider.kt @@ -0,0 +1,475 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Typeface +import android.net.Uri +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import com.novel.read.App +import com.novel.read.constant.AppPattern +import com.novel.read.data.db.entity.Book +import com.novel.read.data.db.entity.BookChapter +import com.novel.read.data.read.TextChapter +import com.novel.read.data.read.TextChar +import com.novel.read.data.read.TextLine +import com.novel.read.data.read.TextPage +import com.novel.read.help.AppConfig +import com.novel.read.help.ReadBookConfig +import com.novel.read.utils.NetworkUtils +import com.novel.read.utils.RealPathUtil +import com.novel.read.utils.ext.* +import java.util.* + + +@Suppress("DEPRECATION") +object ChapterProvider { + private var viewWidth = 0 + private var viewHeight = 0 + var paddingLeft = 0 + var paddingTop = 0 + var visibleWidth = 0 + var visibleHeight = 0 + var visibleRight = 0 + var visibleBottom = 0 + private var lineSpacingExtra = 0 + private var paragraphSpacing = 0 + private var titleTopSpacing = 0 + private var titleBottomSpacing = 0 + var typeface: Typeface = Typeface.SANS_SERIF + lateinit var titlePaint: TextPaint + lateinit var contentPaint: TextPaint + + init { + upStyle() + } + + /** + * 获取拆分完的章节数据 + */ + fun getTextChapter( + book: Book, + bookChapter: BookChapter, + contents: List, + chapterSize: Int, + imageStyle: String?, + ): TextChapter { + val textPages = arrayListOf() + val pageLines = arrayListOf() + val pageLengths = arrayListOf() + val stringBuilder = StringBuilder() + var durY = 0f + textPages.add(TextPage()) + contents.forEachIndexed { index, text -> + val matcher = AppPattern.imgPattern.matcher(text) + if (matcher.find()) { + var src = matcher.group(1) +// if (!book.isEpub()) { + src = NetworkUtils.getAbsoluteURL("", src) +// } + src?.let { + durY = + setTypeImage( + book, bookChapter, src, durY, textPages, imageStyle + ) + } + } else { + val isTitle = index == 0 + if (!(isTitle && ReadBookConfig.titleMode == 2)) { + durY = + setTypeText( + text, durY, textPages, pageLines, + pageLengths, stringBuilder, isTitle + ) + } + } + } + textPages.last().height = durY + 20.dp + textPages.last().text = stringBuilder.toString() + if (pageLines.size < textPages.size) { + pageLines.add(textPages.last().textLines.size) + } + if (pageLengths.size < textPages.size) { + pageLengths.add(textPages.last().text.length) + } + textPages.forEachIndexed { index, item -> + item.index = index + item.pageSize = textPages.size + item.chapterIndex = bookChapter.chapterIndex + item.chapterSize = chapterSize + item.title = bookChapter.chapterName + item.upLinesPosition() + } + + return TextChapter( + bookChapter.chapterIndex, + bookChapter.chapterName, + bookChapter.chapterId.toInt(), + textPages, + pageLines, + pageLengths, + chapterSize + ) + } + + private fun setTypeImage( + book: Book, + chapter: BookChapter, + src: String, + y: Float, + textPages: ArrayList, + imageStyle: String?, + ): Float { + var durY = y + ImageProvider.getImage(book, chapter.chapterId.toInt(), src)?.let { + if (durY > visibleHeight) { + textPages.last().height = durY + textPages.add(TextPage()) + durY = 0f + } + var height = it.height + var width = it.width + when (imageStyle?.toUpperCase(Locale.ROOT)) { + "FULL" -> { + width = visibleWidth + height = it.height * visibleWidth / it.width + } + else -> { + if (it.width > visibleWidth) { + height = it.height * visibleWidth / it.width + width = visibleWidth + } + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + if (durY + height > visibleHeight) { + textPages.last().height = durY + textPages.add(TextPage()) + durY = 0f + } + } + } + val textLine = TextLine(isImage = true) + textLine.lineTop = durY + durY += height + textLine.lineBottom = durY + val (start, end) = if (visibleWidth > width) { + val adjustWidth = (visibleWidth - width) / 2f + Pair( + paddingLeft.toFloat() + adjustWidth, + paddingLeft.toFloat() + adjustWidth + width + ) + } else { + Pair(paddingLeft.toFloat(), (paddingLeft + width).toFloat()) + } + textLine.textChars.add( + TextChar( + charData = src, + start = start, + end = end, + isImage = true + ) + ) + textPages.last().textLines.add(textLine) + } + return durY + paragraphSpacing / 10f + } + + /** + * 排版文字 + */ + private fun setTypeText( + text: String, + y: Float, + textPages: ArrayList, + pageLines: ArrayList, + pageLengths: ArrayList, + stringBuilder: StringBuilder, + isTitle: Boolean, + ): Float { + var durY = if (isTitle) y + titleTopSpacing else y + val textPaint = if (isTitle) titlePaint else contentPaint + val layout = StaticLayout( + text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true + ) + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine(isTitle = isTitle) + val words = + text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) + val desiredWidth = layout.getLineWidth(lineIndex) + var isLastLine = false + if (lineIndex == 0 && layout.lineCount > 1 && !isTitle) { + //第一行 + textLine.text = words + addCharsToLineFirst( + textLine, + words.toStringArray(), + textPaint, + desiredWidth + ) + } else if (lineIndex == layout.lineCount - 1) { + //最后一行 + textLine.text = "$words\n" + isLastLine = true + val x = if (isTitle && ReadBookConfig.titleMode == 1) + (visibleWidth - layout.getLineWidth(lineIndex)) / 2 + else 0f + addCharsToLineLast( + textLine, + words.toStringArray(), + textPaint, + x + ) + } else { + //中间行 + textLine.text = words + addCharsToLineMiddle( + textLine, + words.toStringArray(), + textPaint, + desiredWidth, + 0f + ) + } + if (durY + textPaint.textHeight > visibleHeight) { + //当前页面结束,设置各种值 + textPages.last().text = stringBuilder.toString() + pageLines.add(textPages.last().textLines.size) + pageLengths.add(textPages.last().text.length) + textPages.last().height = durY + //新建页面 + textPages.add(TextPage()) + stringBuilder.clear() + durY = 0f + } + stringBuilder.append(words) + if (isLastLine) stringBuilder.append("\n") + textPages.last().textLines.add(textLine) + textLine.upTopBottom(durY, textPaint) + durY += textPaint.textHeight * lineSpacingExtra / 10f + textPages.last().height = durY + } + if (isTitle) durY += titleBottomSpacing + durY += textPaint.textHeight * paragraphSpacing / 10f + return durY + } + + /** + * 有缩进,两端对齐 + */ + private fun addCharsToLineFirst( + textLine: TextLine, + words: Array, + textPaint: TextPaint, + desiredWidth: Float, + ) { + var x = 0f + if (!ReadBookConfig.textFullJustify) { + addCharsToLineLast( + textLine, + words, + textPaint, + x + ) + return + } + val bodyIndent = ReadBookConfig.paragraphIndent + val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length + bodyIndent.toStringArray().forEach { + val x1 = x + icw + textLine.addTextChar( + charData = it, + start = paddingLeft + x, + end = paddingLeft + x1 + ) + x = x1 + } + val words1 = words.copyOfRange(bodyIndent.length, words.size) + addCharsToLineMiddle( + textLine, + words1, + textPaint, + desiredWidth, + x + ) + } + + /** + * 无缩进,两端对齐 + */ + private fun addCharsToLineMiddle( + textLine: TextLine, + words: Array, + textPaint: TextPaint, + desiredWidth: Float, + startX: Float, + ) { + if (!ReadBookConfig.textFullJustify) { + addCharsToLineLast( + textLine, + words, + textPaint, + startX + ) + return + } + val gapCount: Int = words.lastIndex + val d = (visibleWidth - desiredWidth) / gapCount + var x = startX + words.forEachIndexed { index, s -> + val cw = StaticLayout.getDesiredWidth(s, textPaint) + val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) + textLine.addTextChar( + charData = s, + start = paddingLeft + x, + end = paddingLeft + x1 + ) + x = x1 + } + exceed( + textLine, + words + ) + } + + /** + * 最后一行,自然排列 + */ + private fun addCharsToLineLast( + textLine: TextLine, + words: Array, + textPaint: TextPaint, + startX: Float, + ) { + var x = startX + words.forEach { + val cw = StaticLayout.getDesiredWidth(it, textPaint) + val x1 = x + cw + textLine.addTextChar( + charData = it, + start = paddingLeft + x, + end = paddingLeft + x1 + ) + x = x1 + } + exceed( + textLine, + words + ) + } + + /** + * 超出边界处理 + */ + private fun exceed(textLine: TextLine, words: Array) { + val endX = textLine.textChars.last().end + if (endX > visibleRight) { + val cc = (endX - visibleRight) / words.size + for (i in 0..words.lastIndex) { + textLine.getTextCharReverseAt(i).let { + val py = cc * (words.size - i) + it.start = it.start - py + it.end = it.end - py + } + } + } + } + + /** + * 更新样式 + */ + fun upStyle() { + typeface = try { + val fontPath = ReadBookConfig.textFont + when { + fontPath.isContentPath() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + val fd = App.INSTANCE.contentResolver + .openFileDescriptor(Uri.parse(fontPath), "r")!! + .fileDescriptor + Typeface.Builder(fd).build() + } + fontPath.isContentPath() -> { + Typeface.createFromFile(RealPathUtil.getPath(App.INSTANCE, Uri.parse(fontPath))) + } + fontPath.isNotEmpty() -> Typeface.createFromFile(fontPath) + else -> when (AppConfig.systemTypefaces) { + 1 -> Typeface.SERIF + 2 -> Typeface.MONOSPACE + else -> Typeface.SANS_SERIF + } + } + } catch (e: Exception) { + ReadBookConfig.textFont = "" + ReadBookConfig.save() + Typeface.SANS_SERIF + } + // 字体统一处理 + val bold = Typeface.create(typeface, Typeface.BOLD) + val normal = Typeface.create(typeface, Typeface.NORMAL) + val (titleFont, textFont) = when (ReadBookConfig.textBold) { + 1 -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + Pair(Typeface.create(typeface, 900, false), bold) + else + Pair(bold, bold) + } + 2 -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + Pair(normal, Typeface.create(typeface, 300, false)) + else + Pair(normal, normal) + } + else -> Pair(bold, normal) + } + + //标题 + titlePaint = TextPaint() + titlePaint.color = ReadBookConfig.textColor + titlePaint.letterSpacing = ReadBookConfig.letterSpacing + titlePaint.typeface = titleFont + titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat() + titlePaint.isAntiAlias = true + //正文 + contentPaint = TextPaint() + contentPaint.color = ReadBookConfig.textColor + contentPaint.letterSpacing = ReadBookConfig.letterSpacing + contentPaint.typeface = textFont + contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat() + contentPaint.isAntiAlias = true + //间距 + lineSpacingExtra = ReadBookConfig.lineSpacingExtra + paragraphSpacing = ReadBookConfig.paragraphSpacing + titleTopSpacing = ReadBookConfig.titleTopSpacing.dp + titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dp + upVisibleSize() + } + + /** + * 更新View尺寸 + */ + fun upViewSize(width: Int, height: Int) { + if (width > 0 && height > 0) { + viewWidth = width + viewHeight = height + upVisibleSize() + } + } + + /** + * 更新绘制尺寸 + */ + private fun upVisibleSize() { + if (viewWidth > 0 && viewHeight > 0) { + paddingLeft = ReadBookConfig.paddingLeft.dp + paddingTop = ReadBookConfig.paddingTop.dp + visibleWidth = viewWidth - paddingLeft - ReadBookConfig.paddingRight.dp + visibleHeight = viewHeight - paddingTop - ReadBookConfig.paddingBottom.dp + visibleRight = paddingLeft + visibleWidth + visibleBottom = paddingTop + visibleHeight + } + } + + val TextPaint.textHeight: Float + get() = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading +} diff --git a/app/src/main/java/com/novel/read/ui/read/provider/ImageProvider.kt b/app/src/main/java/com/novel/read/ui/read/provider/ImageProvider.kt new file mode 100644 index 0000000..718c08a --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/read/provider/ImageProvider.kt @@ -0,0 +1,84 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Bitmap +import com.novel.read.data.db.entity.Book +import com.novel.read.help.BookHelp +import com.novel.read.utils.BitmapUtils +import kotlinx.coroutines.runBlocking +import java.util.concurrent.ConcurrentHashMap + +object ImageProvider { + + private val cache = ConcurrentHashMap>() + + @Synchronized + fun getCache(chapterIndex: Int, src: String): Bitmap? { + return cache[chapterIndex]?.get(src) + } + + @Synchronized + fun setCache(chapterIndex: Int, src: String, bitmap: Bitmap) { + var indexCache = cache[chapterIndex] + if (indexCache == null) { + indexCache = ConcurrentHashMap() + cache[chapterIndex] = indexCache + } + indexCache[src] = bitmap + } + + fun getImage(book: Book, chapterIndex: Int, src: String, onUi: Boolean = false): Bitmap? { + getCache(chapterIndex, src)?.let { + return it + } + val vFile = BookHelp.getImage(book, src) + if (!vFile.exists()) { + //todo epub +// if (book.isEpub()) { +// EPUBFile.getImage(book, src)?.use { input -> +// val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath) +// FileOutputStream(newFile).use { output -> +// input.copyTo(output) +// } +// } +// } else + + if (!onUi) { + runBlocking { + BookHelp.saveImage(book, src) + } + } + } + return try { + val bitmap = BitmapUtils.decodeBitmap( + vFile.absolutePath, + ChapterProvider.visibleWidth, + ChapterProvider.visibleHeight + ) + setCache(chapterIndex, src, bitmap) + bitmap + } catch (e: Exception) { + null + } + } + + fun clearAllCache() { + cache.forEach { indexCache -> + indexCache.value.forEach { + it.value.recycle() + } + } + cache.clear() + } + + fun clearOut(chapterIndex: Int) { + cache.forEach { indexCache -> + if (indexCache.key !in chapterIndex - 1..chapterIndex + 1) { + indexCache.value.forEach { + it.value.recycle() + } + cache.remove(indexCache.key) + } + } + } + +} diff --git a/app/src/main/java/com/novel/read/ui/record/ReadRecordActivity.kt b/app/src/main/java/com/novel/read/ui/record/ReadRecordActivity.kt new file mode 100644 index 0000000..9fa8000 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/record/ReadRecordActivity.kt @@ -0,0 +1,69 @@ +package com.novel.read.ui.record + +import android.os.Bundle +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BaseActivity +import com.novel.read.data.db.entity.ReadRecord +import kotlinx.android.synthetic.main.activity_read_record.* +import kotlinx.android.synthetic.main.item_book_common.view.tv_book_name +import kotlinx.android.synthetic.main.item_read_record.view.* + +class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { + lateinit var adapter: RecordAdapter + private var sortMode = 0 + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initData() + } + + private fun initData() { + val readRecords = App.db.getReadRecordDao().getAll() + adapter.setList(readRecords) + } + + private fun initView() { + adapter = RecordAdapter() + recycler_view.adapter = adapter + } + + + inner class RecordAdapter : + BaseQuickAdapter(R.layout.item_read_record) { + override fun convert(holder: BaseViewHolder, item: ReadRecord) { + holder.itemView.run { + tv_book_name.text = item.bookName + tv_read_time.text = formatDuring(item.readTime) + } + } + +// private fun sureDelAlert(item: ReadRecord) { +// alert(R.string.delete) { +// setMessage(getString(R.string.sure_del_any, item.bookName)) +// okButton { +// App.db.getReadRecordDao().delete(item) +// initData() +// } +// noButton() +// }.show() +// } + + private fun formatDuring(mss: Long): String { + val days = mss / (1000 * 60 * 60 * 24) + val hours = mss % (1000 * 60 * 60 * 24) / (1000 * 60 * 60) + val minutes = mss % (1000 * 60 * 60) / (1000 * 60) + val seconds = mss % (1000 * 60) / 1000 + val d = if (days > 0) "${days}天" else "" + val h = if (hours > 0) "${hours}小时" else "" + val m = if (minutes > 0) "${minutes}分钟" else "" + val s = if (seconds > 0) "${seconds}秒" else "" + var time = "$d$h$m$s" + if (time.isBlank()) { + time = "0秒" + } + return time + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/search/HistoryAdapter.kt b/app/src/main/java/com/novel/read/ui/search/HistoryAdapter.kt new file mode 100644 index 0000000..6ef5a86 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/search/HistoryAdapter.kt @@ -0,0 +1,17 @@ +package com.novel.read.ui.search + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.db.entity.SearchHistory +import kotlinx.android.synthetic.main.item_label.view.* + +class HistoryAdapter : BaseQuickAdapter(R.layout.item_label) { + + override fun convert(holder: BaseViewHolder, item: SearchHistory) { + holder.itemView.run { + tv_label.text = item.getBKey() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/search/HotAdapter.kt b/app/src/main/java/com/novel/read/ui/search/HotAdapter.kt new file mode 100644 index 0000000..cf686f1 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/search/HotAdapter.kt @@ -0,0 +1,19 @@ +package com.novel.read.ui.search + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.constant.AppConst +import com.novel.read.utils.StringUtils +import kotlinx.android.synthetic.main.item_label.view.* + +class HotAdapter : BaseQuickAdapter(R.layout.item_label) { + + override fun convert(holder: BaseViewHolder, item: String) { + holder.itemView.run { + tv_label.text = StringUtils.convertCC(item) + tv_label.setBackgroundColor(AppConst.tagColors[holder.adapterPosition % 8]) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/search/SearchActivity.kt b/app/src/main/java/com/novel/read/ui/search/SearchActivity.kt new file mode 100644 index 0000000..dc3504f --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/search/SearchActivity.kt @@ -0,0 +1,235 @@ +package com.novel.read.ui.search + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.library.adapter.base.listener.OnLoadMoreListener +import com.google.android.flexbox.AlignItems +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexWrap +import com.google.android.flexbox.FlexboxLayoutManager +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.VMBaseActivity +import com.novel.read.constant.AppConst +import com.novel.read.lib.ATH +import com.novel.read.lib.dialogs.alert +import com.novel.read.lib.dialogs.noButton +import com.novel.read.lib.dialogs.yesButton +import com.novel.read.utils.ext.applyTint +import com.novel.read.utils.ext.getViewModel +import com.novel.read.utils.ext.requestInputMethod +import kotlinx.android.synthetic.main.activity_daily.* +import kotlinx.android.synthetic.main.activity_search.* +import kotlinx.android.synthetic.main.view_search.* + +//这个页面为了赶进度写的很蠢 +class SearchActivity : VMBaseActivity(R.layout.activity_search) { + + private lateinit var mHisAdapter: HistoryAdapter + private lateinit var mHotAdapter: HotAdapter + private lateinit var mSearchAdapter: SearchAdapter + + override val viewModel: SearchViewModel + get() = getViewModel(SearchViewModel::class.java) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ATH.applyEdgeEffectColor(rlv_history) + ATH.applyEdgeEffectColor(rlv_hot) + ATH.applyEdgeEffectColor(rlv_history) + viewModel.initData() + initHotAdapter() + initHisAdapter() + initSearchAdapter() + initRecycleData() + initClick() + initLoadMore() + } + + private fun initClick() { + tv_search.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.toString().trim { it <= ' ' } == "") { + head_hot.visibility = View.VISIBLE + head_history.visibility = View.VISIBLE + rlv_hot.visibility = View.VISIBLE + rlv_history.visibility = View.VISIBLE + rlv_search.visibility=View.GONE + } else { + head_hot.visibility = View.GONE + head_history.visibility = View.GONE + rlv_hot.visibility = View.GONE + rlv_history.visibility = View.GONE + viewModel.page = 1 + viewModel.searchKey = s.toString().trim() + onRefresh() + rlv_search.visibility=View.VISIBLE + } + + } + + override fun afterTextChanged(s: Editable) { + + } + }) + + //键盘的搜索 + tv_search.setOnKeyListener { v, keyCode, event -> + //修改回车键功能 + if (keyCode == KeyEvent.KEYCODE_ENTER) { + saveKey() + return@setOnKeyListener true + } + false + } + + tv_cancel.setOnClickListener { + onBackPressed() + } + + head_history.setOnClickListener { + alert(title = "操作提示", message = "確認清空全部記錄?") { + yesButton { + App.db.getSearchDao().deleteAll() + viewModel.getHisKey() + } + noButton { + + } + }.show().applyTint().requestInputMethod() + } + } + + private fun initRecycleData() { + viewModel.run { + hisKey.observe(this@SearchActivity) { + mHisAdapter.setList(it) + } + hotKey.observe(this@SearchActivity) { + if (it.searchTermsList.size > 8) { + mHotAdapter.setList(it.searchTermsList.subList(0, 7)) + } else { + mHotAdapter.setList(it.searchTermsList) + } + } + searchList.observe(this@SearchActivity) { + mSearchAdapter.setList(it) + } + pageStatus.observe(this@SearchActivity) { + when (it) { + AppConst.loading -> { + mSearchAdapter.isUseEmpty = true + } + AppConst.complete -> { + mSearchAdapter.isUseEmpty = false + } + AppConst.loadMore -> { + mSearchAdapter.loadMoreModule.isEnableLoadMore = true + } + AppConst.loadComplete -> { + mSearchAdapter.loadMoreModule.loadMoreComplete() + } + AppConst.noMore -> { + mSearchAdapter.loadMoreModule.loadMoreEnd() + } + AppConst.loadMoreFail -> { + mSearchAdapter.loadMoreModule.loadMoreFail() + } + else -> { + mSearchAdapter.setList(null) + mSearchAdapter.setEmptyView(getErrorView()) + mSearchAdapter.isUseEmpty = true + } + } + } + } + } + + private fun initSearchAdapter() { + rlv_search.layoutManager = LinearLayoutManager(this) + mSearchAdapter = SearchAdapter() + rlv_search.adapter = mSearchAdapter + + mSearchAdapter.setOnItemChildClickListener { adapter, view, position -> + tv_search.setText(viewModel.mList[position].getBBookName()) + saveKey() + } + } + + private fun initHisAdapter() { + val manager2 = FlexboxLayoutManager(this) + //设置主轴排列方式 + manager2.flexDirection = FlexDirection.ROW + //设置是否换行 + manager2.flexWrap = FlexWrap.WRAP + manager2.alignItems = AlignItems.STRETCH + mHisAdapter = HistoryAdapter() + rlv_history.layoutManager = manager2 + rlv_history.adapter = mHisAdapter + + mHisAdapter.setOnItemClickListener { adapter, view, position -> + rlv_search.visibility = View.VISIBLE + tv_search.setText(viewModel.hisKey.value?.get(position)?.getBKey()) + saveKey() + } + } + + private fun initHotAdapter() { + val manager = FlexboxLayoutManager(this) + //设置主轴排列方式 + manager.flexDirection = FlexDirection.ROW + //设置是否换行 + manager.flexWrap = FlexWrap.WRAP + manager.alignItems = AlignItems.STRETCH + rlv_hot.layoutManager = manager + mHotAdapter = HotAdapter() + rlv_hot.adapter = mHotAdapter + mHotAdapter.setOnItemClickListener { adapter, view, position -> + rlv_search.visibility = View.VISIBLE + tv_search.setText(mHotAdapter.data[position]) + saveKey() + } + } + + private fun initLoadMore() { + mSearchAdapter.loadMoreModule.setOnLoadMoreListener(object : OnLoadMoreListener { + override fun onLoadMore() { + if (viewModel.pageStatus.value == AppConst.loadMore) { + return + } + viewModel.loadMore() + } + }) + mSearchAdapter.loadMoreModule.isAutoLoadMore = true + mSearchAdapter.loadMoreModule.isEnableLoadMoreIfNotFullPage = false + } + + private fun saveKey() { + val key = tv_search.text.toString().trim { it <= ' ' } + viewModel.saveKey(key) + } + + private fun onRefresh() { + mSearchAdapter.setEmptyView(R.layout.view_loading) + viewModel.searchKey = tv_search.text.toString().trim() + viewModel.searchBook() + } + + private fun getErrorView(): View { + val errorView: View = layoutInflater.inflate(R.layout.view_net_error, rlv_daily, false) + errorView.setOnClickListener { onRefresh() } + return errorView + } + + override fun onBackPressed() { + super.onBackPressed() + overridePendingTransition(R.anim.message_fade_in, R.anim.message_fade_out) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/search/SearchAdapter.kt b/app/src/main/java/com/novel/read/ui/search/SearchAdapter.kt new file mode 100644 index 0000000..6060418 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/search/SearchAdapter.kt @@ -0,0 +1,32 @@ +package com.novel.read.ui.search + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.novel.read.R +import com.novel.read.data.model.SearchResp +import com.novel.read.ui.info.BookInfoActivity +import kotlinx.android.synthetic.main.item_book_common.view.* +import kotlinx.android.synthetic.main.item_init_search.view.tv_book_name +import org.jetbrains.anko.sdk27.listeners.onClick + +class SearchAdapter : BaseQuickAdapter(R.layout.item_book_common), + LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: SearchResp) { + holder.itemView.run { + iv_cover.load(item.coverImageUrl, item.getBBookName(), item.getBAuthorName()) + tv_book_name.text = item.getBBookName() + tv_book_description.text = item.getBIntroduction() + tv_book_author.text = item.getBAuthorName() + tv_word.text = context.getString(R.string.book_word, item.wordCount / 10000) + tv_category.text = item.getBCategoryName() + onClick { + BookInfoActivity.actionBookInfo(context,item.bookId,item.bookTypeId) + } + + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/search/SearchViewModel.kt b/app/src/main/java/com/novel/read/ui/search/SearchViewModel.kt new file mode 100644 index 0000000..27bab05 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/search/SearchViewModel.kt @@ -0,0 +1,80 @@ +package com.novel.read.ui.search + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import com.novel.read.App +import com.novel.read.base.BaseViewModel +import com.novel.read.constant.AppConst +import com.novel.read.data.db.entity.SearchHistory +import com.novel.read.data.model.Pagination +import com.novel.read.data.model.SearchResp +import com.novel.read.network.repository.SearchRepository + +class SearchViewModel(application: Application) : BaseViewModel(application) { + + private val searchRepository by lazy { SearchRepository() } + var searchList = MutableLiveData>() + var hotKey = MutableLiveData>() + var hisKey = MutableLiveData>() + val pageStatus = MutableLiveData() + lateinit var mList: MutableList + var searchKey: String = "" + var page: Int = 1 + var pageSize: Int = 20 + + fun initData() { + getHisKey() + getHotKey() + } + + fun searchBook() { + launch(block = { + pageStatus.value = AppConst.loading + val data = searchRepository.searchBook(page, pageSize, searchKey) + mList = data.bookList + searchList.value = mList + if (pageSize > mList.size) { + pageStatus.value = AppConst.noMore + } + pageStatus.value = AppConst.complete + }, error = { + pageStatus.value = AppConst.error + }) + } + + fun loadMore() { + launch(block = { + pageStatus.value = AppConst.loadMore + page++ + val mData = searchRepository.searchBook(page, pageSize, searchKey).bookList + mList.addAll(mData) + searchList.value = mList + pageStatus.value = AppConst.loadComplete + if (pageSize > mData.size) { + pageStatus.value = AppConst.noMore + } + }, error = { + page-- + pageStatus.value = AppConst.loadMoreFail + }) + } + + private fun getHotKey() { + launch({ + hotKey.value = searchRepository.hotKey() + }, { + }) + } + + fun getHisKey() { + hisKey.postValue(App.db.getSearchDao().getListByTime()) + } + + fun saveKey(key: String) { + val searchListTable = SearchHistory() + searchListTable.key = key + searchListTable.saveTime = System.currentTimeMillis() + App.db.getSearchDao().insert(searchListTable, key) + hisKey.postValue(App.db.getSearchDao().getListByTime()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/setting/OtherConfigFragment.kt b/app/src/main/java/com/novel/read/ui/setting/OtherConfigFragment.kt new file mode 100644 index 0000000..d293e6e --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/setting/OtherConfigFragment.kt @@ -0,0 +1,131 @@ +package com.novel.read.ui.setting + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.observe +import androidx.preference.Preference +import com.allenliu.versionchecklib.v2.AllenVersionChecker +import com.allenliu.versionchecklib.v2.builder.UIData +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BasePreferenceFragment +import com.novel.read.constant.EventBus +import com.novel.read.constant.PreferKey +import com.novel.read.data.model.AppUpdateResp +import com.novel.read.help.AppConfig +import com.novel.read.help.BookHelp +import com.novel.read.lib.ATH +import com.novel.read.lib.dialogs.alert +import com.novel.read.lib.dialogs.noButton +import com.novel.read.lib.dialogs.okButton +import com.novel.read.network.repository.HomeRepository +import com.novel.read.utils.FileUtils +import com.novel.read.utils.LanguageUtils +import com.novel.read.utils.ext.* +import com.novel.read.help.coroutine.Coroutine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlin.coroutines.CoroutineContext + + +class OtherConfigFragment : BasePreferenceFragment(), CoroutineScope by MainScope(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val homeRepository by lazy { HomeRepository() } + var appResp = MutableLiveData() + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_other) + findPreference("check_update")?.summary = + "${getString(R.string.version)} ${App.versionName}" + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + ATH.applyEdgeEffectColor(listView) + appResp.observe(viewLifecycleOwner) { + updateApk(it) + } + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + when (preference?.key) { + PreferKey.cleanCache -> clearCache() + "check_update" -> { + appUpdate() + } + } + return super.onPreferenceTreeClick(preference) + } + + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.showRss -> postEvent(EventBus.SHOW_RSS, "") + PreferKey.language -> { + val lg = sharedPreferences?.all?.get("language") + if (lg == "zh") { + AppConfig.chineseConverterType = 1 + } else if (lg == "tw") { + AppConfig.chineseConverterType = 2 + } + LanguageUtils.setConfiguration(App.INSTANCE) + postEvent(EventBus.RECREATE, "") + } + + } + } + + private fun appUpdate() { + execute { + appResp.value = homeRepository.appUpdate() + } + } + + private fun updateApk(resp: AppUpdateResp?) { + val versionBean = resp?.appEdition + if (versionBean != null) { + val builder = AllenVersionChecker + .getInstance() + .downloadOnly( + UIData.create() + .setTitle(getString(R.string.new_version, versionBean.editionCode)) + .setContent(versionBean.upgradeContent) + .setDownloadUrl(versionBean.fileUrl) + ) + builder.executeMission(activity) + } + } + + fun execute( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine.async(scope, context) { block() } + } + + private fun clearCache() { + requireContext().alert( + titleResource = R.string.clear_cache, + messageResource = R.string.sure_del + ) { + okButton { + BookHelp.clearCache() + FileUtils.deleteFile(requireActivity().cacheDir.absolutePath) + toast(R.string.clear_cache_success) + } + noButton() + }.show().applyTint() + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/setting/SettingActivity.kt b/app/src/main/java/com/novel/read/ui/setting/SettingActivity.kt new file mode 100644 index 0000000..ceaefa8 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/setting/SettingActivity.kt @@ -0,0 +1,26 @@ +package com.novel.read.ui.setting + +import android.os.Bundle +import com.novel.read.R +import com.novel.read.base.BaseActivity +import com.novel.read.constant.EventBus +import com.novel.read.utils.ext.observeEvent + +class SettingActivity : BaseActivity(R.layout.activity_setting_activity) { + + override fun onActivityCreated(savedInstanceState: Bundle?) { + val fTag = "otherConfigFragment" + var configFragment = supportFragmentManager.findFragmentByTag(fTag) + if (configFragment == null) configFragment = OtherConfigFragment() + supportFragmentManager.beginTransaction() + .replace(R.id.configFrameLayout, configFragment, fTag) + .commit() + } + + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.RECREATE) { + recreate() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/theme/ThemeActivity.kt b/app/src/main/java/com/novel/read/ui/theme/ThemeActivity.kt new file mode 100644 index 0000000..b4f601b --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/theme/ThemeActivity.kt @@ -0,0 +1,11 @@ +package com.novel.read.ui.theme + +import android.os.Bundle +import com.novel.read.R +import com.novel.read.base.BaseActivity + +class ThemeActivity : BaseActivity(R.layout.activity_theme) { + override fun onActivityCreated(savedInstanceState: Bundle?) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/welcome/WelcomeActivity.kt b/app/src/main/java/com/novel/read/ui/welcome/WelcomeActivity.kt new file mode 100644 index 0000000..15fbf71 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/welcome/WelcomeActivity.kt @@ -0,0 +1,74 @@ +package com.novel.read.ui.welcome + +import android.Manifest +import android.content.Intent +import android.os.Bundle +import com.hankcs.hanlp.HanLP +import com.permissionx.guolindev.PermissionX +import com.novel.read.R +import com.novel.read.base.BaseActivity +import com.novel.read.help.AppConfig +import com.novel.read.ui.MainActivity +import com.novel.read.ui.read.ReadBookActivity +import com.novel.read.utils.ext.getPrefBoolean +import com.novel.read.help.coroutine.Coroutine +import kotlinx.android.synthetic.main.activity_welcome.* +import org.jetbrains.anko.startActivity +import org.jetbrains.anko.toast + +class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { + + private var flag = false + + override fun onActivityCreated(savedInstanceState: Bundle?) { + // 避免从桌面启动程序后,会重新实例化入口类的activity + if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) { + finish() + } else { + init() + } + } + + private fun init() { + Coroutine.async { + //初始化简繁转换引擎 + when (AppConfig.chineseConverterType) { + 1 -> HanLP.convertToSimplifiedChinese("初始化") + 2 -> HanLP.convertToTraditionalChinese("初始化") + else -> null + } + } + + PermissionX.init(this) + .permissions( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + .request { allGranted, grantedList, deniedList -> + if (allGranted) { + root_view.postDelayed({ startMainActivity() }, 2500) + } else { + toast("权限被拒绝,将导致部分功能异常,请到设置中开启相关权限") +// finish() + } + } + + tvSkip.setOnClickListener { startMainActivity() } + } + + @Synchronized + private fun startMainActivity() { + if (!flag) { + startActivity() + if (getPrefBoolean(R.string.pk_default_read)) { + startActivity() + } + finish() + } + } + + override fun onDestroy() { + flag = true + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/ATECheckBox.kt b/app/src/main/java/com/novel/read/ui/widget/ATECheckBox.kt new file mode 100644 index 0000000..46c72f4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/ATECheckBox.kt @@ -0,0 +1,17 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatCheckBox +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.accentColor + +/** + * @author Aidan Follestad (afollestad) + */ +class ATECheckBox(context: Context, attrs: AttributeSet) : AppCompatCheckBox(context, attrs) { + + init { + ATH.setTint(this, context.accentColor) + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/ATERadioNoButton.kt b/app/src/main/java/com/novel/read/ui/widget/ATERadioNoButton.kt new file mode 100644 index 0000000..29293ac --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/ATERadioNoButton.kt @@ -0,0 +1,67 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatRadioButton +import com.novel.read.R +import com.novel.read.lib.Selector +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* + +/** + * @author Aidan Follestad (afollestad) + */ +class ATERadioNoButton(context: Context, attrs: AttributeSet) : + AppCompatRadioButton(context, attrs) { + + private val isBottomBackground: Boolean + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ATERadioNoButton) + isBottomBackground = + typedArray.getBoolean(R.styleable.ATERadioNoButton_isBottomBackground, false) + typedArray.recycle() + initTheme() + } + + private fun initTheme() { + when { + isInEditMode -> Unit + isBottomBackground -> { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val textColor = context.getPrimaryTextColor(isLight) + background = Selector.shapeBuild() + .setCornerRadius(1.dp) + .setStrokeWidth(1.dp) + .setCheckedBgColor(context.accentColor) + .setCheckedStrokeColor(context.accentColor) + .setDefaultStrokeColor(textColor) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(textColor) + .setCheckedColor(context.getPrimaryTextColor(ColorUtils.isColorLight(context.accentColor))) + .create() + ) + } + else -> { + val textColor = context.getCompatColor(R.color.primaryText) + background = Selector.shapeBuild() + .setCornerRadius(1.dp) + .setStrokeWidth(1.dp) + .setCheckedBgColor(context.accentColor) + .setCheckedStrokeColor(context.accentColor) + .setDefaultStrokeColor(textColor) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(textColor) + .setCheckedColor(context.getPrimaryTextColor(ColorUtils.isColorLight(context.accentColor))) + .create() + ) + } + } + + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/ATESeekBar.kt b/app/src/main/java/com/novel/read/ui/widget/ATESeekBar.kt new file mode 100644 index 0000000..fb8c215 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/ATESeekBar.kt @@ -0,0 +1,17 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatSeekBar +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.accentColor + +/** + * @author Aidan Follestad (afollestad) + */ +class ATESeekBar(context: Context, attrs: AttributeSet) : AppCompatSeekBar(context, attrs) { + + init { + ATH.setTint(this, context.accentColor) + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/ATESwitch.kt b/app/src/main/java/com/novel/read/ui/widget/ATESwitch.kt new file mode 100644 index 0000000..bc3b228 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/ATESwitch.kt @@ -0,0 +1,21 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.SwitchCompat +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.accentColor + +/** + * @author Aidan Follestad (afollestad) + */ +class ATESwitch(context: Context, attrs: AttributeSet) : SwitchCompat(context, attrs) { + + init { + if (!isInEditMode) { + ATH.setTint(this, context.accentColor) + } + + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/AccentBgTextView.kt b/app/src/main/java/com/novel/read/ui/widget/AccentBgTextView.kt new file mode 100644 index 0000000..72439d3 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/AccentBgTextView.kt @@ -0,0 +1,48 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import com.novel.read.R +import com.novel.read.lib.Selector +import com.novel.read.lib.ThemeStore +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* + +class AccentBgTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + private var radius = 0 + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AccentBgTextView) + radius = typedArray.getDimensionPixelOffset(R.styleable.AccentBgTextView_radius, radius) + typedArray.recycle() + upBackground() + setTextColor(Color.WHITE) + } + + fun setRadius(radius: Int) { + this.radius = radius.dp + upBackground() + } + + private fun upBackground() { + background = if (isInEditMode) { + Selector.shapeBuild() + .setCornerRadius(radius) + .setDefaultBgColor(context.getCompatColor(R.color.accent)) + .setPressedBgColor(ColorUtils.darkenColor(context.getCompatColor(R.color.accent))) + .create() + } else { + Selector.shapeBuild() + .setCornerRadius(radius) + .setDefaultBgColor(ThemeStore.accentColor(context)) + .setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) + .create() + } + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/AccentTextView.kt b/app/src/main/java/com/novel/read/ui/widget/AccentTextView.kt new file mode 100644 index 0000000..ccc72ea --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/AccentTextView.kt @@ -0,0 +1,22 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import com.novel.read.R +import com.novel.read.utils.ext.accentColor +import org.jetbrains.anko.textColor +import org.jetbrains.anko.textColorResource + +class AccentTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + init { + if (!isInEditMode) { + textColor = context.accentColor + } else { + textColorResource = R.color.accent + } + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/AutoCompleteTextView.kt b/app/src/main/java/com/novel/read/ui/widget/AutoCompleteTextView.kt new file mode 100644 index 0000000..ffa1f73 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/AutoCompleteTextView.kt @@ -0,0 +1,71 @@ +package com.novel.read.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import com.novel.read.R +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.* +import kotlinx.android.synthetic.main.item_1line_text_and_del.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +@Suppress("unused") +class AutoCompleteTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatAutoCompleteTextView(context, attrs) { + + var delCallBack: ((value: String) -> Unit)? = null + + init { + ATH.applyAccentTint(this) + } + + override fun enoughToFilter(): Boolean { + return true + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event?.action == MotionEvent.ACTION_DOWN) { + showDropDown() + } + return super.onTouchEvent(event) + } + + fun setFilterValues(values: List?) { + values?.let { + setAdapter(MyAdapter(context, values)) + } + } + + fun setFilterValues(vararg value: String) { + setAdapter(MyAdapter(context, value.toMutableList())) + } + + inner class MyAdapter(context: Context, values: List) : + ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, values) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: LayoutInflater.from(context) + .inflate(R.layout.item_1line_text_and_del, parent, false) + view.text_view.text = getItem(position) + if (delCallBack != null) view.iv_delete.visible() else view.iv_delete.gone() + view.iv_delete.onClick { + getItem(position)?.let { + remove(it) + delCallBack?.invoke(it) + showDropDown() + } + } + return view + } + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/BadgeView.kt b/app/src/main/java/com/novel/read/ui/widget/BadgeView.kt new file mode 100644 index 0000000..2ebb39e --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/BadgeView.kt @@ -0,0 +1,229 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.text.TextUtils +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.FrameLayout.LayoutParams +import androidx.appcompat.widget.AppCompatTextView +import com.novel.read.R +import com.novel.read.utils.ext.* + + +/** + * Created by milad heydari on 5/6/2016. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class BadgeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + var isHideOnNull = true + set(hideOnNull) { + field = hideOnNull + text = text + } + private var radius: Float = 0.toFloat() + private var flatangle: Boolean + + val badgeCount: Int? + get() { + if (text == null) { + return null + } + val text = text.toString() + return try { + Integer.parseInt(text) + } catch (e: NumberFormatException) { + null + } + + } + + var badgeGravity: Int + get() { + val params = layoutParams as LayoutParams + return params.gravity + } + set(gravity) { + val params = layoutParams as LayoutParams + params.gravity = gravity + layoutParams = params + } + + val badgeMargin: IntArray + get() { + val params = layoutParams as LayoutParams + return intArrayOf( + params.leftMargin, + params.topMargin, + params.rightMargin, + params.bottomMargin + ) + } + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BadgeView) + val radios = + typedArray.getDimensionPixelOffset(R.styleable.BadgeView_radius, 8) + flatangle = + typedArray.getBoolean(R.styleable.BadgeView_up_flat_angle, false) + typedArray.recycle() + + if (layoutParams !is LayoutParams) { + val layoutParams = LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + setLayoutParams(layoutParams) + } + + // set default font + setTextColor(Color.WHITE) + //setTypeface(Typeface.DEFAULT_BOLD); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) + setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f)) + radius = radios.toFloat() + + // set default background + setBackground(radius, context.accentColor) + + gravity = Gravity.CENTER + + // default values + isHideOnNull = true + setBadgeCount(0) + minWidth = dip2Px(16f) + minHeight = dip2Px(16f) + } + + fun setBackground(dipRadius: Float, badgeColor: Int) { + val radius = dip2Px(dipRadius).toFloat() + val radiusArray = + floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius) + if (flatangle) { radiusArray.fill(0f, 0, 3) } + + val roundRect = RoundRectShape(radiusArray, null, null) + val bgDrawable = ShapeDrawable(roundRect) + bgDrawable.paint.color = badgeColor + background = bgDrawable + } + + fun setBackground(badgeColor: Int) { + setBackground(radius, badgeColor) + } + + /** + * @see android.widget.TextView.setText + */ + override fun setText(text: CharSequence, type: BufferType) { + if (isHideOnNull && TextUtils.isEmpty(text)) { + gone() + } else { + visible() + } + super.setText(text, type) + } + + fun setBadgeCount(count: Int) { + text = count.toString() + if (count == 0) { + gone() + } else { + visible() + } + } + + fun setHighlight(highlight: Boolean) { + if (highlight) { + setBackground(context.accentColor) + } else { + setBackground(context.getCompatColor(R.color.darker_gray)) + } + } + + fun setBadgeMargin(dipMargin: Int) { + setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin) + } + + fun setBadgeMargin( + leftDipMargin: Int, + topDipMargin: Int, + rightDipMargin: Int, + bottomDipMargin: Int + ) { + val params = layoutParams as LayoutParams + params.leftMargin = dip2Px(leftDipMargin.toFloat()) + params.topMargin = dip2Px(topDipMargin.toFloat()) + params.rightMargin = dip2Px(rightDipMargin.toFloat()) + params.bottomMargin = dip2Px(bottomDipMargin.toFloat()) + layoutParams = params + } + + fun incrementBadgeCount(increment: Int) { + val count = badgeCount + if (count == null) { + setBadgeCount(increment) + } else { + setBadgeCount(increment + count) + } + } + + fun decrementBadgeCount(decrement: Int) { + incrementBadgeCount(-decrement) + } + + /** + * Attach the BadgeView to the target view + * @param target the view to attach the BadgeView + */ + fun setTargetView(target: View?) { + if (parent != null) { + (parent as ViewGroup).removeView(this) + } + + if (target == null) { + return + } + + if (target.parent is FrameLayout) { + (target.parent as FrameLayout).addView(this) + + } else if (target.parent is ViewGroup) { + // use a new FrameLayout container for adding badge + val parentContainer = target.parent as ViewGroup + val groupIndex = parentContainer.indexOfChild(target) + parentContainer.removeView(target) + + val badgeContainer = FrameLayout(context) + val parentLayoutParams = target.layoutParams + + badgeContainer.layoutParams = parentLayoutParams + target.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams) + badgeContainer.addView(target) + + badgeContainer.addView(this) + } + + } + + /** + * converts dip to px + */ + private fun dip2Px(dip: Float): Int { + return (dip * context.resources.displayMetrics.density + 0.5f).toInt() + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/BatteryView.kt b/app/src/main/java/com/novel/read/ui/widget/BatteryView.kt new file mode 100644 index 0000000..ba89588 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/BatteryView.kt @@ -0,0 +1,65 @@ +package com.novel.read.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface +import android.util.AttributeSet +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatTextView +import com.novel.read.utils.ext.dp +import java.io.File + +class BatteryView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + private val batteryPaint = Paint() + private val outFrame = Rect() + private val polar = Rect() + var isBattery = false + + init { + setPadding(4.dp, 0, 6.dp, 0) + batteryPaint.strokeWidth = 1.dp.toFloat() + batteryPaint.isAntiAlias = true + batteryPaint.color = paint.color + typeface = Typeface.createFromAsset(context.assets, "font${File.separator}number.ttf") + } + + fun setColor(@ColorInt color: Int) { + setTextColor(color) + batteryPaint.color = color + invalidate() + } + + @SuppressLint("SetTextI18n") + fun setBattery(battery: Int) { + text = "$battery" + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (!isBattery) return + outFrame.set( + 1.dp, + layout.getLineBaseline(0) + layout.getLineAscent(0) + 2.dp, + width - 3.dp, + layout.getLineBaseline(0) + 2.dp + ) + val dj = (outFrame.bottom - outFrame.top) / 3 + polar.set( + outFrame.right, + outFrame.top + dj, + width - 1.dp, + outFrame.bottom - dj + ) + batteryPaint.style = Paint.Style.STROKE + canvas.drawRect(outFrame, batteryPaint) + batteryPaint.style = Paint.Style.FILL + canvas.drawRect(polar, batteryPaint) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/EditText.kt b/app/src/main/java/com/novel/read/ui/widget/EditText.kt new file mode 100644 index 0000000..c61185d --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/EditText.kt @@ -0,0 +1,17 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatEditText +import com.novel.read.lib.ATH +import com.novel.read.lib.ThemeStore + + +class EditText(context: Context, attrs: AttributeSet) : AppCompatEditText(context, attrs) { + + init { + if (!isInEditMode) { + ATH.setTint(this, ThemeStore.accentColor(context)) + } + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/HeadLayout.java b/app/src/main/java/com/novel/read/ui/widget/HeadLayout.java new file mode 100644 index 0000000..1272057 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/HeadLayout.java @@ -0,0 +1,69 @@ +package com.novel.read.ui.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.novel.read.R; + + +public class HeadLayout extends LinearLayout { + + private TextView mTvHead; + private TextView mTvMore; + public HeadLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + LayoutInflater.from(context).inflate(R.layout.widget_head, this, true); + initViews(); + initAttr(attrs); + } + + + private void initViews() { + mTvHead=findViewById(R.id.tv_head); + mTvMore = findViewById(R.id.tv_more); + } + private void initAttr(AttributeSet attrs) { + TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.HeadLayout); + boolean showHead = typedArray.getBoolean(R.styleable.HeadLayout_showHead, true); + int headText=typedArray.getResourceId(R.styleable.HeadLayout_headText,R.string.empty_info); + int headImg= typedArray.getResourceId(R.styleable.HeadLayout_headImg, R.mipmap.ic_launcher); + boolean showRight = typedArray.getBoolean(R.styleable.HeadLayout_showRightText, true); + int rightText=typedArray.getResourceId(R.styleable.HeadLayout_rightText,R.string.empty_info); + int rightImg=typedArray.getResourceId(R.styleable.HeadLayout_rightImg,R.mipmap.ic_launcher); + if (showHead){ + mTvHead.setText(headText); + Drawable drawable = ContextCompat.getDrawable(getContext(), headImg); + drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight()); + mTvHead.setCompoundDrawables(drawable, null, null, null); + }else { + mTvHead.setVisibility(GONE); + } + if (showRight){ + mTvMore.setText(rightText); + if (rightImg!=R.mipmap.ic_launcher){ + Drawable drawable = ContextCompat.getDrawable(getContext(), rightImg); + drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight()); + mTvHead.setCompoundDrawables(null, null, drawable, null); + } + }else { + mTvMore.setVisibility(GONE); + } + + typedArray.recycle(); + } + + //设置点击右边文字的监听 + public void setRightTextClickListener(OnClickListener listener) { + mTvMore.setOnClickListener(listener); + } + + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/InertiaScrollTextView.kt b/app/src/main/java/com/novel/read/ui/widget/InertiaScrollTextView.kt new file mode 100644 index 0000000..6cd9909 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/InertiaScrollTextView.kt @@ -0,0 +1,228 @@ +package com.novel.read.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.ViewConfiguration +import android.view.animation.Interpolator +import android.widget.OverScroller +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.ViewCompat +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + + +@Suppress("unused") +open class InertiaScrollTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + private val scrollStateIdle = 0 + private val scrollStateDragging = 1 + val scrollStateSettling = 2 + + private val mViewFling: ViewFling by lazy { ViewFling() } + private var velocityTracker: VelocityTracker? = null + private var mScrollState = scrollStateIdle + private var mLastTouchY: Int = 0 + private var mTouchSlop: Int = 0 + private var mMinFlingVelocity: Int = 0 + private var mMaxFlingVelocity: Int = 0 + + //滑动距离的最大边界 + private var mOffsetHeight: Int = 0 + + //f(x) = (x-1)^5 + 1 + private val sQuinticInterpolator = Interpolator { + var t = it + t -= 1.0f + t * t * t * t * t + 1.0f + } + + init { + val vc = ViewConfiguration.get(context) + mTouchSlop = vc.scaledTouchSlop + mMinFlingVelocity = vc.scaledMinimumFlingVelocity + mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + } + + fun atTop(): Boolean { + return scrollY <= 0 + } + + fun atBottom(): Boolean { + return scrollY >= mOffsetHeight + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + initOffsetHeight() + } + + override fun onTextChanged( + text: CharSequence?, + start: Int, + lengthBefore: Int, + lengthAfter: Int + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + initOffsetHeight() + } + + private fun initOffsetHeight() { + val mLayoutHeight: Int + + //获得内容面板 + val mLayout = layout ?: return + //获得内容面板的高度 + mLayoutHeight = mLayout.height + + //计算滑动距离的边界 + mOffsetHeight = mLayoutHeight + totalPaddingTop + totalPaddingBottom - measuredHeight + } + + override fun scrollTo(x: Int, y: Int) { + super.scrollTo(x, min(y, mOffsetHeight)) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + event?.let { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(it) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + setScrollState(scrollStateIdle) + mLastTouchY = (event.y + 0.5f).toInt() + } + MotionEvent.ACTION_MOVE -> { + val y = (event.y + 0.5f).toInt() + var dy = mLastTouchY - y + if (mScrollState != scrollStateDragging) { + var startScroll = false + + if (abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop + } else { + dy += mTouchSlop + } + startScroll = true + } + if (startScroll) { + setScrollState(scrollStateDragging) + } + } + if (mScrollState == scrollStateDragging) { + mLastTouchY = y + } + } + MotionEvent.ACTION_UP -> { + velocityTracker?.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat()) + val yVelocity = velocityTracker?.yVelocity ?: 0f + if (abs(yVelocity) > mMinFlingVelocity) { + mViewFling.fling(-yVelocity.toInt()) + } else { + setScrollState(scrollStateIdle) + } + resetTouch() + } + MotionEvent.ACTION_CANCEL -> { + resetTouch() + } + } + } + return super.onTouchEvent(event) + } + + private fun resetTouch() { + velocityTracker?.clear() + } + + private fun setScrollState(state: Int) { + if (state == mScrollState) { + return + } + mScrollState = state + if (state != scrollStateSettling) { + mViewFling.stop() + } + } + + /** + * 惯性滚动 + */ + private inner class ViewFling : Runnable { + + private var mLastFlingY = 0 + private val mScroller: OverScroller = OverScroller(context, sQuinticInterpolator) + private var mEatRunOnAnimationRequest = false + private var mReSchedulePostAnimationCallback = false + + override fun run() { + disableRunOnAnimationRequests() + val scroller = mScroller + if (scroller.computeScrollOffset()) { + val y = scroller.currY + val dy = y - mLastFlingY + mLastFlingY = y + if (dy < 0 && scrollY > 0) { + scrollBy(0, max(dy, -scrollY)) + } else if (dy > 0 && scrollY < mOffsetHeight) { + scrollBy(0, min(dy, mOffsetHeight - scrollY)) + } + postOnAnimation() + } + enableRunOnAnimationRequests() + } + + fun fling(velocityY: Int) { + mLastFlingY = 0 + setScrollState(scrollStateSettling) + mScroller.fling( + 0, + 0, + 0, + velocityY, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + Integer.MIN_VALUE, + Integer.MAX_VALUE + ) + postOnAnimation() + } + + fun stop() { + removeCallbacks(this) + mScroller.abortAnimation() + } + + private fun disableRunOnAnimationRequests() { + mReSchedulePostAnimationCallback = false + mEatRunOnAnimationRequest = true + } + + private fun enableRunOnAnimationRequests() { + mEatRunOnAnimationRequest = false + if (mReSchedulePostAnimationCallback) { + postOnAnimation() + } + } + + fun postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true + } else { + removeCallbacks(this) + ViewCompat.postOnAnimation(this@InertiaScrollTextView, this) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/RefreshProgressBar.kt b/app/src/main/java/com/novel/read/ui/widget/RefreshProgressBar.kt new file mode 100644 index 0000000..06f0e38 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/RefreshProgressBar.kt @@ -0,0 +1,197 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.os.Looper +import android.util.AttributeSet +import android.view.View +import com.novel.read.R + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class RefreshProgressBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + private var a = 1 + private var durProgress = 0 + private var secondDurProgress = 0 + var maxProgress = 100 + var secondMaxProgress = 100 + var bgColor = 0x00000000 + var secondColor = -0x3e3e3f + var fontColor = -0xc9c9ca + var speed = 2 + var secondFinalProgress = 0 + private set + private var paint: Paint = Paint() + private val bgRect = Rect() + private val secondRect = Rect() + private val fontRectF = RectF() + + var isAutoLoading: Boolean = false + set(loading) { + field = loading + if (!loading) { + secondDurProgress = 0 + secondFinalProgress = 0 + } + maxProgress = 0 + + invalidate() + } + + init { + paint.style = Paint.Style.FILL + + val a = context.obtainStyledAttributes(attrs, R.styleable.RefreshProgressBar) + speed = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_speed, speed) + maxProgress = a.getInt(R.styleable.RefreshProgressBar_max_progress, maxProgress) + durProgress = a.getInt(R.styleable.RefreshProgressBar_dur_progress, durProgress) + secondDurProgress = a.getDimensionPixelSize( + R.styleable.RefreshProgressBar_second_dur_progress, + secondDurProgress + ) + secondFinalProgress = secondDurProgress + secondMaxProgress = a.getDimensionPixelSize( + R.styleable.RefreshProgressBar_second_max_progress, + secondMaxProgress + ) + bgColor = a.getColor(R.styleable.RefreshProgressBar_bg_color, bgColor) + secondColor = a.getColor(R.styleable.RefreshProgressBar_second_color, secondColor) + fontColor = a.getColor(R.styleable.RefreshProgressBar_font_color, fontColor) + a.recycle() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + paint.color = bgColor + bgRect.set(0, 0, measuredWidth, measuredHeight) + canvas.drawRect(bgRect, paint) + + if (secondDurProgress > 0 && secondMaxProgress > 0) { + var secondDur = secondDurProgress + if (secondDur < 0) { + secondDur = 0 + } + if (secondDur > secondMaxProgress) { + secondDur = secondMaxProgress + } + paint.color = secondColor + val tempW = + (measuredWidth.toFloat() * 1.0f * (secondDur * 1.0f / secondMaxProgress)).toInt() + secondRect.set( + measuredWidth / 2 - tempW / 2, + 0, + measuredWidth / 2 + tempW / 2, + measuredHeight + ) + canvas.drawRect(secondRect, paint) + } + + if (durProgress > 0 && maxProgress > 0) { + paint.color = fontColor + fontRectF.set( + 0f, + 0f, + measuredWidth.toFloat() * 1.0f * (durProgress * 1.0f / maxProgress), + measuredHeight.toFloat() + ) + canvas.drawRect(fontRectF, paint) + } + + if (this.isAutoLoading) { + if (secondDurProgress >= secondMaxProgress) { + a = -1 + } else if (secondDurProgress <= 0) { + a = 1 + } + secondDurProgress += a * speed + if (secondDurProgress < 0) + secondDurProgress = 0 + else if (secondDurProgress > secondMaxProgress) + secondDurProgress = secondMaxProgress + secondFinalProgress = secondDurProgress + invalidate() + } else { + if (secondDurProgress != secondFinalProgress) { + if (secondDurProgress > secondFinalProgress) { + secondDurProgress -= speed + if (secondDurProgress < secondFinalProgress) { + secondDurProgress = secondFinalProgress + } + } else { + secondDurProgress += speed + if (secondDurProgress > secondFinalProgress) { + secondDurProgress = secondFinalProgress + } + } + this.invalidate() + } + } + } + + fun getDurProgress(): Int { + return durProgress + } + + fun setDurProgress(durProgress: Int) { + var durProgress1 = durProgress + if (durProgress1 < 0) { + durProgress1 = 0 + } + if (durProgress1 > maxProgress) { + durProgress1 = maxProgress + } + this.durProgress = durProgress1 + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } + + fun getSecondDurProgress(): Int { + return secondDurProgress + } + + fun setSecondDurProgress(secondDur: Int) { + this.secondDurProgress = secondDur + this.secondFinalProgress = secondDurProgress + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } + + fun setSecondDurProgressWithAnim(secondDur: Int) { + var secondDur1 = secondDur + if (secondDur1 < 0) { + secondDur1 = 0 + } + if (secondDur1 > secondMaxProgress) { + secondDur1 = secondMaxProgress + } + this.secondFinalProgress = secondDur1 + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } + + fun clean() { + durProgress = 0 + secondDurProgress = 0 + secondFinalProgress = 0 + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/SearchView.kt b/app/src/main/java/com/novel/read/ui/widget/SearchView.kt new file mode 100644 index 0000000..958b1c1 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/SearchView.kt @@ -0,0 +1,104 @@ +package com.novel.read.ui.widget + +import android.annotation.SuppressLint +import android.app.SearchableInfo +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ImageSpan +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.widget.TextView +import androidx.appcompat.widget.SearchView +import com.novel.read.R + +class SearchView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : SearchView(context, attrs) { + private var mSearchHintIcon: Drawable? = null + private var textView: TextView? = null + + @SuppressLint("UseCompatLoadingForDrawables") + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + super.onLayout(changed, left, top, right, bottom) + try { + if (textView == null) { + textView = findViewById(androidx.appcompat.R.id.search_src_text) + mSearchHintIcon = this.context.getDrawable(R.drawable.ic_search_hint) + updateQueryHint() + } + // 改变字体 + textView!!.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + textView!!.gravity = Gravity.CENTER_VERTICAL + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun getDecoratedHint(hintText: CharSequence): CharSequence { + // If the field is always expanded or we don't have a search hint icon, + // then don't add the search icon to the hint. + if (mSearchHintIcon == null) { + return hintText + } + val textSize = (textView!!.textSize * 0.8).toInt() + mSearchHintIcon!!.setBounds(0, 0, textSize, textSize) + val ssb = SpannableStringBuilder(" ") + ssb.setSpan(CenteredImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + ssb.append(hintText) + return ssb + } + + private fun updateQueryHint() { + textView?.let { + it.hint = getDecoratedHint(queryHint ?: "") + } + } + + override fun setIconifiedByDefault(iconified: Boolean) { + super.setIconifiedByDefault(iconified) + updateQueryHint() + } + + override fun setSearchableInfo(searchable: SearchableInfo?) { + super.setSearchableInfo(searchable) + searchable?.let { + updateQueryHint() + } + } + + override fun setQueryHint(hint: CharSequence?) { + super.setQueryHint(hint) + updateQueryHint() + } + + internal class CenteredImageSpan(drawable: Drawable?) : ImageSpan(drawable!!) { + override fun draw( + canvas: Canvas, text: CharSequence, + start: Int, end: Int, x: Float, + top: Int, y: Int, bottom: Int, paint: Paint + ) { + // image to draw + val b = drawable + // font metrics of text to be replaced + val fm = paint.fontMetricsInt + val transY = ((y + fm.descent + y + fm.ascent) / 2 + - b.bounds.bottom / 2) + canvas.save() + canvas.translate(x, transY.toFloat()) + b.draw(canvas) + canvas.restore() + } + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/SmoothCheckBox.kt b/app/src/main/java/com/novel/read/ui/widget/SmoothCheckBox.kt new file mode 100644 index 0000000..823cda3 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/SmoothCheckBox.kt @@ -0,0 +1,326 @@ +package com.novel.read.ui.widget + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import android.view.animation.LinearInterpolator +import android.widget.Checkable +import com.novel.read.R +import com.novel.read.lib.ThemeStore +import com.novel.read.utils.ext.dp +import com.novel.read.utils.ext.getCompatColor +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sqrt + +class SmoothCheckBox @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs), Checkable { + private var mPaint: Paint + private var mTickPaint: Paint + private var mFloorPaint: Paint + private var mTickPoints: Array + private var mCenterPoint: Point + private var mTickPath: Path + private var mLeftLineDistance = 0f + private var mRightLineDistance = 0f + private var mDrewDistance = 0f + private var mScaleVal = 1.0f + private var mFloorScale = 1.0f + private var mWidth = 0 + private var mAnimDuration = 0 + private var mStrokeWidth = 0 + private var mCheckedColor = 0 + private var mUnCheckedColor = 0 + private var mFloorColor = 0 + private var mFloorUnCheckedColor = 0 + private var mChecked = false + private var mTickDrawing = false + var onCheckedChangeListener: ((checkBox: SmoothCheckBox, isChecked: Boolean) -> Unit)? = null + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox) + var tickColor = ThemeStore.accentColor(context) + mCheckedColor = context.getCompatColor(R.color.background_menu) + mUnCheckedColor = context.getCompatColor(R.color.background_menu) + mFloorColor = context.getCompatColor(R.color.transparent30) + tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, tickColor) + mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION) + mFloorColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, mFloorColor) + mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, mCheckedColor) + mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, mUnCheckedColor) + mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, 0) + ta.recycle() + mFloorUnCheckedColor = mFloorColor + mTickPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mTickPaint.style = Paint.Style.STROKE + mTickPaint.strokeCap = Paint.Cap.ROUND + mTickPaint.color = tickColor + mFloorPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mFloorPaint.style = Paint.Style.FILL + mFloorPaint.color = mFloorColor + mPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mPaint.style = Paint.Style.FILL + mPaint.color = mCheckedColor + mTickPath = Path() + mCenterPoint = Point() + mTickPoints = arrayOf(Point(), Point(), Point()) + setOnClickListener { + toggle() + mTickDrawing = false + mDrewDistance = 0f + if (isChecked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + } + } + + override fun isChecked(): Boolean { + return mChecked + } + + override fun setChecked(checked: Boolean) { + mChecked = checked + reset() + invalidate() + onCheckedChangeListener?.invoke(this@SmoothCheckBox, mChecked) + } + + override fun toggle() { + this.isChecked = !isChecked + } + + /** + * checked with animation + * + * @param checked checked + * @param animate change with animation + */ + fun setChecked(checked: Boolean, animate: Boolean) { + if (animate) { + mTickDrawing = false + mChecked = checked + mDrewDistance = 0f + if (checked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + onCheckedChangeListener?.invoke(this@SmoothCheckBox, mChecked) + } else { + this.isChecked = checked + } + } + + private fun reset() { + mTickDrawing = true + mFloorScale = 1.0f + mScaleVal = if (isChecked) 0f else 1.0f + mFloorColor = if (isChecked) mCheckedColor else mFloorUnCheckedColor + mDrewDistance = if (isChecked) mLeftLineDistance + mRightLineDistance else 0f + } + + private fun measureSize(measureSpec: Int): Int { + val defSize: Int = DEF_DRAW_SIZE.dp + val specSize = MeasureSpec.getSize(measureSpec) + val specMode = MeasureSpec.getMode(measureSpec) + var result = 0 + when (specMode) { + MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> result = min(defSize, specSize) + MeasureSpec.EXACTLY -> result = specSize + } + return result + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)) + } + + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + mWidth = measuredWidth + mStrokeWidth = if (mStrokeWidth == 0) measuredWidth / 10 else mStrokeWidth + mStrokeWidth = + if (mStrokeWidth > measuredWidth / 5) measuredWidth / 5 else mStrokeWidth + mStrokeWidth = if (mStrokeWidth < 3) 3 else mStrokeWidth + mCenterPoint.x = mWidth / 2 + mCenterPoint.y = measuredHeight / 2 + mTickPoints[0].x = (measuredWidth.toFloat() / 30 * 7).roundToInt() + mTickPoints[0].y = (measuredHeight.toFloat() / 30 * 14).roundToInt() + mTickPoints[1].x = (measuredWidth.toFloat() / 30 * 13).roundToInt() + mTickPoints[1].y = (measuredHeight.toFloat() / 30 * 20).roundToInt() + mTickPoints[2].x = (measuredWidth.toFloat() / 30 * 22).roundToInt() + mTickPoints[2].y = (measuredHeight.toFloat() / 30 * 10).roundToInt() + mLeftLineDistance = sqrt( + (mTickPoints[1].x - mTickPoints[0].x.toDouble()).pow(2.0) + + (mTickPoints[1].y - mTickPoints[0].y.toDouble()).pow(2.0) + ).toFloat() + mRightLineDistance = sqrt( + (mTickPoints[2].x - mTickPoints[1].x.toDouble()).pow(2.0) + + (mTickPoints[2].y - mTickPoints[1].y.toDouble()).pow(2.0) + ).toFloat() + mTickPaint.strokeWidth = mStrokeWidth.toFloat() + } + + override fun onDraw(canvas: Canvas) { + drawBorder(canvas) + drawCenter(canvas) + drawTick(canvas) + } + + private fun drawCenter(canvas: Canvas) { + mPaint.color = mUnCheckedColor + val radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal + canvas.drawCircle(mCenterPoint.x.toFloat(), mCenterPoint.y.toFloat(), radius, mPaint) + } + + private fun drawBorder(canvas: Canvas) { + mFloorPaint.color = mFloorColor + val radius = mCenterPoint.x + canvas.drawCircle( + mCenterPoint.x.toFloat(), + mCenterPoint.y.toFloat(), + radius * mFloorScale, + mFloorPaint + ) + } + + private fun drawTick(canvas: Canvas) { + if (mTickDrawing && isChecked) { + drawTickPath(canvas) + } + } + + private fun drawTickPath(canvas: Canvas) { + mTickPath.reset() + // draw left of the tick + if (mDrewDistance < mLeftLineDistance) { + val step: Float = if (mWidth / 20.0f < 3) 3f else mWidth / 20.0f + mDrewDistance += step + val stopX = + mTickPoints[0].x + (mTickPoints[1].x - mTickPoints[0].x) * mDrewDistance / mLeftLineDistance + val stopY = + mTickPoints[0].y + (mTickPoints[1].y - mTickPoints[0].y) * mDrewDistance / mLeftLineDistance + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(stopX, stopY) + canvas.drawPath(mTickPath, mTickPaint) + if (mDrewDistance > mLeftLineDistance) { + mDrewDistance = mLeftLineDistance + } + } else { + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + // draw right of the tick + if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { + val stopX = + mTickPoints[1].x + (mTickPoints[2].x - mTickPoints[1].x) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance + val stopY = + mTickPoints[1].y - (mTickPoints[1].y - mTickPoints[2].y) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance + mTickPath.reset() + mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(stopX, stopY) + canvas.drawPath(mTickPath, mTickPaint) + val step: Float = if (mWidth / 20f < 3) 3f else mWidth / 20f + mDrewDistance += step + } else { + mTickPath.reset() + mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(mTickPoints[2].x.toFloat(), mTickPoints[2].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + } + } + // invalidate + if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { + postDelayed({ this.postInvalidate() }, 10) + } + } + + private fun startCheckedAnimation() { + val animator = ValueAnimator.ofFloat(1.0f, 0f) + animator.duration = mAnimDuration / 3 * 2.toLong() + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { animation: ValueAnimator -> + mScaleVal = animation.animatedValue as Float + mFloorColor = getGradientColor( + mUnCheckedColor, + mCheckedColor, + 1 - mScaleVal + ) + postInvalidate() + } + animator.start() + val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) + floorAnimator.duration = mAnimDuration.toLong() + floorAnimator.interpolator = LinearInterpolator() + floorAnimator.addUpdateListener { animation: ValueAnimator -> + mFloorScale = animation.animatedValue as Float + postInvalidate() + } + floorAnimator.start() + drawTickDelayed() + } + + private fun startUnCheckedAnimation() { + val animator = ValueAnimator.ofFloat(0f, 1.0f) + animator.duration = mAnimDuration.toLong() + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { animation: ValueAnimator -> + mScaleVal = animation.animatedValue as Float + mFloorColor = getGradientColor( + mCheckedColor, + mFloorUnCheckedColor, + mScaleVal + ) + postInvalidate() + } + animator.start() + val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) + floorAnimator.duration = mAnimDuration.toLong() + floorAnimator.interpolator = LinearInterpolator() + floorAnimator.addUpdateListener { animation: ValueAnimator -> + mFloorScale = animation.animatedValue as Float + postInvalidate() + } + floorAnimator.start() + } + + private fun drawTickDelayed() { + postDelayed({ + mTickDrawing = true + postInvalidate() + }, mAnimDuration.toLong()) + } + + companion object { + private const val DEF_DRAW_SIZE = 25 + private const val DEF_ANIM_DURATION = 300 + private fun getGradientColor(startColor: Int, endColor: Int, percent: Float): Int { + val startA = Color.alpha(startColor) + val startR = Color.red(startColor) + val startG = Color.green(startColor) + val startB = Color.blue(startColor) + val endA = Color.alpha(endColor) + val endR = Color.red(endColor) + val endG = Color.green(endColor) + val endB = Color.blue(endColor) + val currentA = (startA * (1 - percent) + endA * percent).toInt() + val currentR = (startR * (1 - percent) + endR * percent).toInt() + val currentG = (startG * (1 - percent) + endG * percent).toInt() + val currentB = (startB * (1 - percent) + endB * percent).toInt() + return Color.argb(currentA, currentR, currentG, currentB) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/StrokeTextView.kt b/app/src/main/java/com/novel/read/ui/widget/StrokeTextView.kt new file mode 100644 index 0000000..9ba4f3e --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/StrokeTextView.kt @@ -0,0 +1,89 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import com.novel.read.R +import com.novel.read.lib.Selector +import com.novel.read.lib.ThemeStore +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* + + +open class StrokeTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + private var radius = 1.dp + private val isBottomBackground: Boolean + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.StrokeTextView) + radius = typedArray.getDimensionPixelOffset(R.styleable.StrokeTextView_radius, radius) + isBottomBackground = + typedArray.getBoolean(R.styleable.StrokeTextView_isBottomBackground, false) + typedArray.recycle() + upBackground() + } + + fun setRadius(radius: Int) { + this.radius = radius.dp + upBackground() + } + + private fun upBackground() { + when { + isInEditMode -> { + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dp) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(context.getCompatColor(R.color.secondaryText)) + .setSelectedStrokeColor(context.getCompatColor(R.color.accent)) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + this.setTextColor( + Selector.colorBuild() + .setDefaultColor(context.getCompatColor(R.color.secondaryText)) + .setSelectedColor(context.getCompatColor(R.color.accent)) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + isBottomBackground -> { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dp) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(context.getPrimaryTextColor(isLight)) + .setSelectedStrokeColor(context.accentColor) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + this.setTextColor( + Selector.colorBuild() + .setDefaultColor(context.getPrimaryTextColor(isLight)) + .setSelectedColor(context.accentColor) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + else -> { + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dp) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(ThemeStore.textColorSecondary(context)) + .setSelectedStrokeColor(ThemeStore.accentColor(context)) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + this.setTextColor( + Selector.colorBuild() + .setDefaultColor(ThemeStore.textColorSecondary(context)) + .setSelectedColor(ThemeStore.accentColor(context)) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + } + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/TitleBar.kt b/app/src/main/java/com/novel/read/ui/widget/TitleBar.kt new file mode 100644 index 0000000..36b40df --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/TitleBar.kt @@ -0,0 +1,219 @@ +package com.novel.read.ui.widget + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Build +import android.util.AttributeSet +import android.view.Menu +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.StyleRes +import androidx.appcompat.widget.Toolbar +import com.google.android.material.appbar.AppBarLayout +import com.novel.read.R +import com.novel.read.help.AppConfig +import com.novel.read.utils.ext.* +import org.jetbrains.anko.backgroundColor +import org.jetbrains.anko.bottomPadding +import org.jetbrains.anko.topPadding + +@Suppress("unused") +class TitleBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppBarLayout(context, attrs) { + + val toolbar: Toolbar + val menu: Menu + get() = toolbar.menu + + var title: CharSequence? + get() = toolbar.title + set(title) { + toolbar.title = title + } + + var subtitle: CharSequence? + get() = toolbar.subtitle + set(subtitle) { + toolbar.subtitle = subtitle + } + + private val displayHomeAsUp: Boolean + private val navigationIconTint: ColorStateList? + private val navigationIconTintMode: Int + private val attachToActivity: Boolean + + init { + val a = context.obtainStyledAttributes( + attrs, R.styleable.TitleBar, + R.attr.titleBarStyle, 0 + ) + navigationIconTint = a.getColorStateList(R.styleable.TitleBar_navigationIconTint) + navigationIconTintMode = a.getInt(R.styleable.TitleBar_navigationIconTintMode, 9) + attachToActivity = a.getBoolean(R.styleable.TitleBar_attachToActivity, true) + displayHomeAsUp = a.getBoolean(R.styleable.TitleBar_displayHomeAsUp, true) + + val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon) + val navigationContentDescription = + a.getText(R.styleable.TitleBar_navigationContentDescription) + val titleText = a.getString(R.styleable.TitleBar_title) + val subtitleText = a.getString(R.styleable.TitleBar_subtitle) + + when (a.getInt(R.styleable.TitleBar_themeMode, 0)) { + 1 -> inflate(context, R.layout.view_title_bar_dark, this) + else -> inflate(context, R.layout.view_title_bar, this) + } + toolbar = findViewById(R.id.toolbar) + + toolbar.apply { + navigationIcon?.let { + this.navigationIcon = it + this.navigationContentDescription = navigationContentDescription + } + + if (a.hasValue(R.styleable.TitleBar_titleTextAppearance)) { + this.setTitleTextAppearance( + context, + a.getResourceId(R.styleable.TitleBar_titleTextAppearance, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_titleTextColor)) { + this.setTitleTextColor(a.getColor(R.styleable.TitleBar_titleTextColor, -0x1)) + } + + if (a.hasValue(R.styleable.TitleBar_subtitleTextAppearance)) { + this.setSubtitleTextAppearance( + context, + a.getResourceId(R.styleable.TitleBar_subtitleTextAppearance, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_subtitleTextColor)) { + this.setSubtitleTextColor(a.getColor(R.styleable.TitleBar_subtitleTextColor, -0x1)) + } + + + if (a.hasValue(R.styleable.TitleBar_contentInsetLeft) + || a.hasValue(R.styleable.TitleBar_contentInsetRight) + ) { + this.setContentInsetsAbsolute( + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetLeft, 0), + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetRight, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_contentInsetStart) + || a.hasValue(R.styleable.TitleBar_contentInsetEnd) + ) { + this.setContentInsetsRelative( + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetStart, 0), + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetEnd, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_contentInsetStartWithNavigation)) { + this.contentInsetStartWithNavigation = a.getDimensionPixelOffset( + R.styleable.TitleBar_contentInsetStartWithNavigation, 0 + ) + } + + if (a.hasValue(R.styleable.TitleBar_contentInsetEndWithActions)) { + this.contentInsetEndWithActions = a.getDimensionPixelOffset( + R.styleable.TitleBar_contentInsetEndWithActions, 0 + ) + } + + if (!titleText.isNullOrBlank()) { + this.title = titleText + } + + if (!subtitleText.isNullOrBlank()) { + this.subtitle = subtitleText + } + + if (a.hasValue(R.styleable.TitleBar_contentLayout)) { + inflate(context, a.getResourceId(R.styleable.TitleBar_contentLayout, 0), this) + } + } + + if (!isInEditMode) { + if (a.getBoolean(R.styleable.TitleBar_fitStatusBar, true)) { + topPadding = context.statusBarHeight + } + + if (a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false)) { + bottomPadding = context.navigationBarHeight + } + + backgroundColor = context.primaryColor + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + stateListAnimator = null + elevation = if (AppConfig.elevation < 0) { + context.elevation + } else { + AppConfig.elevation.toFloat() + } + } + + } + a.recycle() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + attachToActivity() + } + + fun setNavigationOnClickListener(clickListener: ((View) -> Unit)) { + toolbar.setNavigationOnClickListener(clickListener) + } + + fun setTitle(titleId: Int) { + toolbar.setTitle(titleId) + } + + fun setSubTitle(subtitleId: Int) { + toolbar.setSubtitle(subtitleId) + } + + fun setTitleTextColor(@ColorInt color: Int) { + toolbar.setTitleTextColor(color) + } + + fun setTitleTextAppearance(@StyleRes resId: Int) { + toolbar.setTitleTextAppearance(context, resId) + } + + fun setSubTitleTextColor(@ColorInt color: Int) { + toolbar.setSubtitleTextColor(color) + } + + fun setSubTitleTextAppearance(@StyleRes resId: Int) { + toolbar.setSubtitleTextAppearance(context, resId) + } + + fun transparent() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + elevation = 0f + } + backgroundColor = Color.TRANSPARENT + } + + fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, fullScreen: Boolean) { + topPadding = if (!isInMultiWindowMode && fullScreen) context.statusBarHeight else 0 + } + + private fun attachToActivity() { + if (attachToActivity) { + activity?.let { + it.setSupportActionBar(toolbar) + it.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUp) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/UpLinearLayoutManager.kt b/app/src/main/java/com/novel/read/ui/widget/UpLinearLayoutManager.kt new file mode 100644 index 0000000..59d1fbd --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/UpLinearLayoutManager.kt @@ -0,0 +1,39 @@ +package com.novel.read.ui.widget + +import android.content.Context +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class UpLinearLayoutManager(val context: Context) : LinearLayoutManager(context) { + + fun smoothScrollToPosition(position: Int) { + smoothScrollToPosition(position, 0) + } + + fun smoothScrollToPosition(position: Int, offset: Int) { + val scroller = UpLinearSmoothScroller(context) + scroller.targetPosition = position + scroller.offset = offset + startSmoothScroll(scroller) + } +} + +class UpLinearSmoothScroller(context: Context?): LinearSmoothScroller(context) { + var offset = 0 + + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun getHorizontalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int { + if (snapPreference == SNAP_TO_START) { + return boxStart - viewStart + offset + } + throw IllegalArgumentException("snap preference should be SNAP_TO_START") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/VerticalDivider.kt b/app/src/main/java/com/novel/read/ui/widget/VerticalDivider.kt new file mode 100644 index 0000000..11eaecb --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/VerticalDivider.kt @@ -0,0 +1,16 @@ +package com.novel.read.ui.widget + +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import com.novel.read.R + +class VerticalDivider(context: Context) : DividerItemDecoration(context, VERTICAL) { + + init { + ContextCompat.getDrawable(context, R.drawable.ic_divider)?.let { + this.setDrawable(it) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/anim/RotateLoading.kt b/app/src/main/java/com/novel/read/ui/widget/anim/RotateLoading.kt new file mode 100644 index 0000000..046355b --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/anim/RotateLoading.kt @@ -0,0 +1,219 @@ +package com.novel.read.ui.widget.anim + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import com.novel.read.R +import com.novel.read.utils.ext.* + +/** + * RotateLoading + * Created by Victor on 2015/4/28. + */ +@Suppress("MemberVisibilityCanBePrivate") +class RotateLoading @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + + private var mPaint: Paint + + private var loadingRectF: RectF? = null + private var shadowRectF: RectF? = null + + private var topDegree = 10 + private var bottomDegree = 190 + + private var arc: Float = 0.toFloat() + + private var thisWidth: Int = 0 + + private var changeBigger = true + + private var shadowPosition: Int = 0 + + var hideMode = GONE + + var isStarted = false + private set + + var loadingColor: Int = 0 + set(value) { + field = value + invalidate() + } + + private var speedOfDegree: Int = 0 + + private var speedOfArc: Float = 0.toFloat() + + private val shown = Runnable { this.startInternal() } + + private val hidden = Runnable { this.stopInternal() } + + init { + loadingColor = context.accentColor + thisWidth = DEFAULT_WIDTH.dp + shadowPosition = DEFAULT_SHADOW_POSITION.dp + speedOfDegree = DEFAULT_SPEED_OF_DEGREE + + if (null != attrs) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RotateLoading) + loadingColor = + typedArray.getColor(R.styleable.RotateLoading_loading_color, loadingColor) + thisWidth = typedArray.getDimensionPixelSize( + R.styleable.RotateLoading_loading_width, + DEFAULT_WIDTH.dp + ) + shadowPosition = typedArray.getInt(R.styleable.RotateLoading_shadow_position, DEFAULT_SHADOW_POSITION) + speedOfDegree = typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE) + hideMode = when (typedArray.getInt(R.styleable.RotateLoading_hide_mode, 2)) { + 1 -> INVISIBLE + else -> GONE + } + typedArray.recycle() + } + speedOfArc = (speedOfDegree / 4).toFloat() + mPaint = Paint() + mPaint.color = loadingColor + mPaint.isAntiAlias = true + mPaint.style = Paint.Style.STROKE + mPaint.strokeWidth = thisWidth.toFloat() + mPaint.strokeCap = Paint.Cap.ROUND + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + arc = 10f + + loadingRectF = + RectF( + (2 * thisWidth).toFloat(), + (2 * thisWidth).toFloat(), + (w - 2 * thisWidth).toFloat(), + (h - 2 * thisWidth).toFloat() + ) + shadowRectF = RectF( + (2 * thisWidth + shadowPosition).toFloat(), + (2 * thisWidth + shadowPosition).toFloat(), + (w - 2 * thisWidth + shadowPosition).toFloat(), + (h - 2 * thisWidth + shadowPosition).toFloat() + ) + } + + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (!isStarted) { + return + } + + mPaint.color = Color.parseColor("#1a000000") + shadowRectF?.let { + canvas.drawArc(it, topDegree.toFloat(), arc, false, mPaint) + canvas.drawArc(it, bottomDegree.toFloat(), arc, false, mPaint) + } + + mPaint.color = loadingColor + loadingRectF?.let { + canvas.drawArc(it, topDegree.toFloat(), arc, false, mPaint) + canvas.drawArc(it, bottomDegree.toFloat(), arc, false, mPaint) + } + + topDegree += speedOfDegree + bottomDegree += speedOfDegree + if (topDegree > 360) { + topDegree -= 360 + } + if (bottomDegree > 360) { + bottomDegree -= 360 + } + + if (changeBigger) { + if (arc < 160) { + arc += speedOfArc + invalidate() + } + } else { + if (arc > speedOfDegree) { + arc -= 2 * speedOfArc + invalidate() + } + } + if (arc >= 160 || arc <= 10) { + changeBigger = !changeBigger + invalidate() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (visibility == VISIBLE) { + startInternal() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + isStarted = false + animate().cancel() + removeCallbacks(shown) + removeCallbacks(hidden) + } + + fun show() { + removeCallbacks(shown) + removeCallbacks(hidden) + post(shown) + } + + fun hide() { + removeCallbacks(shown) + removeCallbacks(hidden) + stopInternal() + } + + private fun startInternal() { + startAnimator() + isStarted = true + invalidate() + } + + private fun stopInternal() { + stopAnimator() + invalidate() + } + + private fun startAnimator() { + animate().cancel() + animate().scaleX(1.0f) + .scaleY(1.0f) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + visibility = VISIBLE + } + }) + .start() + } + + private fun stopAnimator() { + animate().cancel() + isStarted = false + this.visibility = hideMode + } + + companion object { + private const val DEFAULT_WIDTH = 6 + private const val DEFAULT_SHADOW_POSITION = 2 + private const val DEFAULT_SPEED_OF_DEGREE = 10 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/widget/dialog/AppraiseDialog.kt b/app/src/main/java/com/novel/read/ui/widget/dialog/AppraiseDialog.kt similarity index 90% rename from app/src/main/java/com/novel/read/widget/dialog/AppraiseDialog.kt rename to app/src/main/java/com/novel/read/ui/widget/dialog/AppraiseDialog.kt index 1b6735f..2866d6c 100644 --- a/app/src/main/java/com/novel/read/widget/dialog/AppraiseDialog.kt +++ b/app/src/main/java/com/novel/read/ui/widget/dialog/AppraiseDialog.kt @@ -1,12 +1,10 @@ -package com.novel.read.widget.dialog +package com.novel.read.ui.widget.dialog import android.app.Dialog import android.content.Context -import android.view.LayoutInflater -import android.view.View +import android.view.* import android.widget.LinearLayout import android.widget.TextView - import com.novel.read.R import kotlinx.android.synthetic.main.dialog_go_appraise.* @@ -36,4 +34,4 @@ class AppraiseDialog(context: Context) : Dialog(context, R.style.dialog) { tv_appraise.setOnClickListener(receiveClickListener) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/dialog/AutoReadDialog.kt b/app/src/main/java/com/novel/read/ui/widget/dialog/AutoReadDialog.kt new file mode 100644 index 0000000..a637fe4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/dialog/AutoReadDialog.kt @@ -0,0 +1,111 @@ +package com.novel.read.ui.widget.dialog + +import android.os.Bundle +import android.view.* +import android.widget.SeekBar +import com.novel.read.R +import com.novel.read.base.BaseDialogFragment +import com.novel.read.help.ReadBookConfig +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.help.ReadAloud +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.bottomBackground +import com.novel.read.utils.ext.getPrimaryTextColor +import kotlinx.android.synthetic.main.dialog_auto_read.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class AutoReadDialog : BaseDialogFragment() { + var callBack: CallBack? = null + + override fun onStart() { + super.onStart() + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 0, 0, 0) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + it.attributes = attr + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + callBack = activity as? CallBack + return inflater.inflate(R.layout.dialog_auto_read, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + root_view.setBackgroundColor(bg) + tv_read_speed_title.setTextColor(textColor) + tv_read_speed.setTextColor(textColor) + iv_catalog.setColorFilter(textColor) + tv_catalog.setTextColor(textColor) + iv_main_menu.setColorFilter(textColor) + tv_main_menu.setTextColor(textColor) + iv_auto_page_stop.setColorFilter(textColor) + tv_auto_page_stop.setTextColor(textColor) + iv_setting.setColorFilter(textColor) + tv_setting.setTextColor(textColor) + initOnChange() + initData() + initEvent() + } + + private fun initData() { + val speed = if (ReadBookConfig.autoReadSpeed < 10) 10 else ReadBookConfig.autoReadSpeed + tv_read_speed.text = String.format("%ds", speed) + seek_auto_read.progress = speed + } + + private fun initOnChange() { + seek_auto_read.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + val speed = if (progress < 10) 10 else progress + tv_read_speed.text = String.format("%ds", speed) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar) { + ReadBookConfig.autoReadSpeed = + if (seek_auto_read.progress < 10) 10 else seek_auto_read.progress + upTtsSpeechRate() + } + }) + } + + private fun initEvent() { + ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() } + ll_setting.onClick { + ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") + } + ll_catalog.onClick { callBack?.openChapterList() } + ll_auto_page_stop.onClick { + callBack?.autoPageStop() + dismiss() + } + } + + private fun upTtsSpeechRate() { + ReadAloud.upTtsSpeechRate(requireContext()) + if (!BaseReadAloudService.pause) { + ReadAloud.pause(requireContext()) + ReadAloud.resume(requireContext()) + } + } + + interface CallBack { + fun showMenuBar() + fun openChapterList() + fun autoPageStop() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/dialog/LoginDialog.kt b/app/src/main/java/com/novel/read/ui/widget/dialog/LoginDialog.kt new file mode 100644 index 0000000..98621f8 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/dialog/LoginDialog.kt @@ -0,0 +1,32 @@ +package com.novel.read.ui.widget.dialog + +import android.app.Dialog +import android.content.Context +import android.view.* +import android.widget.LinearLayout +import com.novel.read.R + +class LoginDialog(context: Context) : Dialog(context, R.style.dialog) { + private val view: View = LayoutInflater.from(context).inflate(R.layout.dialog_login, null) + + init { + view.setHasTransientState(true) + setContentView( + view, LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + ) + setCancelable(true) + initView() + + } + + private fun initView() { + + } + + fun LoginDialog(receiveClickListener: View.OnClickListener) { + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/dialog/PhotoDialog.kt b/app/src/main/java/com/novel/read/ui/widget/dialog/PhotoDialog.kt new file mode 100644 index 0000000..a05fa83 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/dialog/PhotoDialog.kt @@ -0,0 +1,68 @@ +package com.novel.read.ui.widget.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import com.novel.read.R +import com.novel.read.base.BaseDialogFragment +import com.novel.read.service.help.ReadBook +import io.legado.app.ui.book.read.page.provider.ImageProvider +import kotlinx.android.synthetic.main.dialog_photo_view.* + +class PhotoDialog : BaseDialogFragment() { + + companion object { + + fun show( + fragmentManager: FragmentManager, + chapterIndex: Int, + src: String + ) { + PhotoDialog().apply { + val bundle = Bundle() + bundle.putInt("chapterIndex", chapterIndex) + bundle.putString("src", src) + arguments = bundle + }.show(fragmentManager, "photoDialog") + } + + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_photo_view, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + arguments?.let { + val chapterIndex = it.getInt("chapterIndex") + val src = it.getString("src") + ReadBook.book?.let { book -> + src?.let { + execute { + ImageProvider.getImage(book, chapterIndex, src) + }.onSuccess { bitmap -> + if (bitmap != null) { + photo_view.setImageBitmap(bitmap) + } + } + } + } + } + + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/dialog/ReadAloudConfigDialog.kt b/app/src/main/java/com/novel/read/ui/widget/dialog/ReadAloudConfigDialog.kt new file mode 100644 index 0000000..b41ff23 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/dialog/ReadAloudConfigDialog.kt @@ -0,0 +1,128 @@ +package com.novel.read.ui.widget.dialog + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.fragment.app.DialogFragment +import androidx.preference.ListPreference +import androidx.preference.Preference +import com.novel.read.App +import com.novel.read.R +import com.novel.read.base.BasePreferenceFragment +import com.novel.read.constant.EventBus +import com.novel.read.constant.PreferKey +import com.novel.read.lib.ATH +import com.novel.read.service.BaseReadAloudService +import com.novel.read.service.help.ReadAloud +import com.novel.read.utils.ext.* + +class ReadAloudConfigDialog : DialogFragment() { + private val readAloudPreferTag = "readAloudPreferTag" + + override fun onStart() { + super.onStart() + val dm = requireActivity().getSize() + dialog?.window?.let { + it.setBackgroundDrawableResource(R.color.transparent) + it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = LinearLayout(requireContext()) + view.setBackgroundColor(requireContext().backgroundColor) + view.id = R.id.tag1 + container?.addView(view) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + var preferenceFragment = childFragmentManager.findFragmentByTag(readAloudPreferTag) + if (preferenceFragment == null) preferenceFragment = ReadAloudPreferenceFragment() + childFragmentManager.beginTransaction() + .replace(view.id, preferenceFragment, readAloudPreferTag) + .commit() + } + + class ReadAloudPreferenceFragment : BasePreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val speakEngineSummary: String + get() { + val eid = App.INSTANCE.getPrefLong(PreferKey.speakEngine) +// val ht = App.db.httpTTSDao().get(eid) +// return ht?.name ?: getString(R.string.local_tts) + return getString(R.string.local_tts) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_aloud) + upPreferenceSummary( + findPreference(PreferKey.speakEngine), + speakEngineSummary + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ATH.applyEdgeEffectColor(listView) + } + + override fun onResume() { + super.onResume() + preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + super.onPause() + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + when (preference?.key) { +// PreferKey.speakEngine -> +// SpeakEngineDialog().show(childFragmentManager, "speakEngine") + } + return super.onPreferenceTreeClick(preference) + } + + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String? + ) { + when (key) { + PreferKey.readAloudByPage -> { + if (BaseReadAloudService.isRun) { + postEvent(EventBus.MEDIA_BUTTON, false) + } + } + PreferKey.speakEngine -> { + upPreferenceSummary(findPreference(key), speakEngineSummary) + ReadAloud.upReadAloudClass() + } + } + } + + private fun upPreferenceSummary(preference: Preference?, value: String) { + when (preference) { + is ListPreference -> { + val index = preference.findIndexOfValue(value) + // Set the summary to reflect the new value. + preference.summary = if (index >= 0) preference.entries[index] else null + } + else -> { + preference?.summary = value + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/dialog/TextDialog.kt b/app/src/main/java/com/novel/read/ui/widget/dialog/TextDialog.kt new file mode 100644 index 0000000..fae31b9 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/dialog/TextDialog.kt @@ -0,0 +1,97 @@ +package com.novel.read.ui.widget.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import com.novel.read.R +import com.novel.read.base.BaseDialogFragment +import com.novel.read.utils.ext.* +import io.noties.markwon.Markwon +import io.noties.markwon.image.glide.GlideImagesPlugin +import kotlinx.android.synthetic.main.dialog_text_view.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class TextDialog : BaseDialogFragment() { + + companion object { + const val MD = 1 + + fun show( + fragmentManager: FragmentManager, + content: String?, + mode: Int = 0, + time: Long = 0, + autoClose: Boolean = false + ) { + TextDialog().apply { + val bundle = Bundle() + bundle.putString("content", content) + bundle.putInt("mode", mode) + bundle.putLong("time", time) + arguments = bundle + isCancelable = false + this.autoClose = autoClose + }.show(fragmentManager, "textDialog") + } + + } + + private var time = 0L + + private var autoClose: Boolean = false + + override fun onStart() { + super.onStart() + val dm = requireActivity().getSize() + dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_text_view, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + arguments?.let { + val content = it.getString("content") ?: "" + when (it.getInt("mode")) { + MD -> text_view.post { + Markwon.builder(requireContext()) + .usePlugin(GlideImagesPlugin.create(requireContext())) + .build() + .setMarkdown(text_view, content) + } + else -> text_view.text = content + } + time = it.getLong("time", 0L) + } + if (time > 0) { + badge_view.setBadgeCount((time / 1000).toInt()) + launch { + while (time > 0) { + delay(1000) + time -= 1000 + badge_view.setBadgeCount((time / 1000).toInt()) + if (time <= 0) { + view.post { + dialog?.setCancelable(true) + if (autoClose) dialog?.cancel() + } + } + } + } + } else { + view.post { + dialog?.setCancelable(true) + if (autoClose) dialog?.cancel() + } + } + } + +} diff --git a/app/src/main/java/com/novel/read/widget/RefreshLayout.java b/app/src/main/java/com/novel/read/ui/widget/dynamic/RefreshLayout.java similarity index 99% rename from app/src/main/java/com/novel/read/widget/RefreshLayout.java rename to app/src/main/java/com/novel/read/ui/widget/dynamic/RefreshLayout.java index d5511d5..e38e141 100644 --- a/app/src/main/java/com/novel/read/widget/RefreshLayout.java +++ b/app/src/main/java/com/novel/read/ui/widget/dynamic/RefreshLayout.java @@ -1,4 +1,4 @@ -package com.novel.read.widget; +package com.novel.read.ui.widget.dynamic; import android.content.Context; import android.content.res.TypedArray; diff --git a/app/src/main/java/com/novel/read/ui/widget/image/CircleImageView.kt b/app/src/main/java/com/novel/read/ui/widget/image/CircleImageView.kt new file mode 100644 index 0000000..c137bc4 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/image/CircleImageView.kt @@ -0,0 +1,466 @@ +package com.novel.read.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.text.TextPaint +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatImageView +import com.novel.read.R +import com.novel.read.utils.ext.getCompatColor +import com.novel.read.utils.ext.sp +import kotlin.math.min +import kotlin.math.pow + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class CircleImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + + private val mDrawableRect = RectF() + private val mBorderRect = RectF() + + private val mShaderMatrix = Matrix() + private val mBitmapPaint = Paint() + private val mBorderPaint = Paint() + private val mCircleBackgroundPaint = Paint() + private val textPaint by lazy { + val textPaint = TextPaint() + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint + } + + private var mBorderColor = DEFAULT_BORDER_COLOR + private var mBorderWidth = DEFAULT_BORDER_WIDTH + private var mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR + + private var mBitmap: Bitmap? = null + private var mBitmapShader: BitmapShader? = null + private var mBitmapWidth: Int = 0 + private var mBitmapHeight: Int = 0 + + private var mDrawableRadius: Float = 0.toFloat() + private var mBorderRadius: Float = 0.toFloat() + + private var mColorFilter: ColorFilter? = null + + private var mReady: Boolean = false + private var mSetupPending: Boolean = false + private var mBorderOverlay: Boolean = false + var isDisableCircularTransformation: Boolean = false + set(disableCircularTransformation) { + if (field == disableCircularTransformation) { + return + } + field = disableCircularTransformation + initializeBitmap() + } + + var borderColor: Int + get() = mBorderColor + set(@ColorInt borderColor) { + if (borderColor == mBorderColor) { + return + } + + mBorderColor = borderColor + mBorderPaint.color = mBorderColor + invalidate() + } + + var circleBackgroundColor: Int + get() = mCircleBackgroundColor + set(@ColorInt circleBackgroundColor) { + if (circleBackgroundColor == mCircleBackgroundColor) { + return + } + mCircleBackgroundColor = circleBackgroundColor + mCircleBackgroundPaint.color = circleBackgroundColor + invalidate() + } + + var borderWidth: Int + get() = mBorderWidth + set(borderWidth) { + if (borderWidth == mBorderWidth) { + return + } + + mBorderWidth = borderWidth + setup() + } + + var isBorderOverlay: Boolean + get() = mBorderOverlay + set(borderOverlay) { + if (borderOverlay == mBorderOverlay) { + return + } + + mBorderOverlay = borderOverlay + setup() + } + + private var text: String? = null + + private var textColor = context.getCompatColor(R.color.primaryText) + private var textBold = false + var isInView = false + + init { + super.setScaleType(SCALE_TYPE) + val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView) + mBorderWidth = + a.getDimensionPixelSize( + R.styleable.CircleImageView_civ_border_width, + DEFAULT_BORDER_WIDTH + ) + mBorderColor = + a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR) + mBorderOverlay = + a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY) + mCircleBackgroundColor = + a.getColor( + R.styleable.CircleImageView_civ_circle_background_color, + DEFAULT_CIRCLE_BACKGROUND_COLOR + ) + text = a.getString(R.styleable.CircleImageView_text) + if (a.hasValue(R.styleable.CircleImageView_textColor)) { + textColor = a.getColor( + R.styleable.CircleImageView_textColor, + context.getCompatColor(R.color.primaryText) + ) + } + a.recycle() + + mReady = true + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + outlineProvider = OutlineProvider() + } + + if (mSetupPending) { + setup() + mSetupPending = false + } + } + + override fun getScaleType(): ScaleType { + return SCALE_TYPE + } + + override fun setScaleType(scaleType: ScaleType) { + if (scaleType != SCALE_TYPE) { + throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)) + } + } + + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + if (adjustViewBounds) { + throw IllegalArgumentException("adjustViewBounds not supported.") + } + } + + override fun onDraw(canvas: Canvas) { + if (isDisableCircularTransformation) { + super.onDraw(canvas) + return + } + if (mBitmap == null) { + return + } + + if (mCircleBackgroundColor != Color.TRANSPARENT) { + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mCircleBackgroundPaint + ) + } + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mBitmapPaint + ) + if (mBorderWidth > 0) { + canvas.drawCircle( + mBorderRect.centerX(), + mBorderRect.centerY(), + mBorderRadius, + mBorderPaint + ) + } + drawText(canvas) + } + + private fun drawText(canvas: Canvas) { + text?.let { + textPaint.color = textColor + textPaint.isFakeBoldText = textBold + textPaint.textSize = 15.sp.toFloat() + val fm = textPaint.fontMetrics + canvas.drawText( + it, + width * 0.5f, + (height * 0.5f + (fm.bottom - fm.top) * 0.5f - fm.bottom), + textPaint + ) + } + } + + fun setText(text: String?) { + this.text = text + invalidate() + } + + fun setTextColor(@ColorInt textColor: Int) { + this.textColor = textColor + invalidate() + } + + fun setTextBold(bold: Boolean) { + this.textBold = bold + invalidate() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + setup() + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(left, top, right, bottom) + setup() + } + + override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { + super.setPaddingRelative(start, top, end, bottom) + setup() + } + + fun setCircleBackgroundColorResource(@ColorRes circleBackgroundRes: Int) { + circleBackgroundColor = context.getCompatColor(circleBackgroundRes) + } + + override fun setImageBitmap(bm: Bitmap) { + super.setImageBitmap(bm) + initializeBitmap() + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + initializeBitmap() + } + + override fun setImageResource(@DrawableRes resId: Int) { + super.setImageResource(resId) + initializeBitmap() + } + + override fun setImageURI(uri: Uri?) { + super.setImageURI(uri) + initializeBitmap() + } + + override fun setColorFilter(cf: ColorFilter) { + if (cf === mColorFilter) { + return + } + + mColorFilter = cf + applyColorFilter() + invalidate() + } + + override fun getColorFilter(): ColorFilter? { + return mColorFilter + } + + private fun applyColorFilter() { + mBitmapPaint.colorFilter = mColorFilter + } + + private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? { + if (drawable == null) { + return null + } + + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + return try { + val bitmap: Bitmap = if (drawable is ColorDrawable) { + Bitmap.createBitmap( + COLOR_DRAWABLE_DIMENSION, + COLOR_DRAWABLE_DIMENSION, + BITMAP_CONFIG + ) + } else { + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + BITMAP_CONFIG + ) + } + + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + bitmap + } catch (e: Exception) { + e.printStackTrace() + null + } + + } + + private fun initializeBitmap() { + mBitmap = if (isDisableCircularTransformation) { + null + } else { + getBitmapFromDrawable(drawable) + } + setup() + } + + private fun setup() { + if (!mReady) { + mSetupPending = true + return + } + + if (width == 0 && height == 0) { + return + } + + if (mBitmap == null) { + invalidate() + return + } + + mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + + mBitmapPaint.isAntiAlias = true + mBitmapPaint.shader = mBitmapShader + + mBorderPaint.style = Paint.Style.STROKE + mBorderPaint.isAntiAlias = true + mBorderPaint.color = mBorderColor + mBorderPaint.strokeWidth = mBorderWidth.toFloat() + + mCircleBackgroundPaint.style = Paint.Style.FILL + mCircleBackgroundPaint.isAntiAlias = true + mCircleBackgroundPaint.color = mCircleBackgroundColor + + mBitmapHeight = mBitmap!!.height + mBitmapWidth = mBitmap!!.width + + mBorderRect.set(calculateBounds()) + mBorderRadius = + min( + (mBorderRect.height() - mBorderWidth) / 2.0f, + (mBorderRect.width() - mBorderWidth) / 2.0f + ) + + mDrawableRect.set(mBorderRect) + if (!mBorderOverlay && mBorderWidth > 0) { + mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f) + } + mDrawableRadius = min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f) + + applyColorFilter() + updateShaderMatrix() + invalidate() + } + + private fun calculateBounds(): RectF { + val availableWidth = width - paddingLeft - paddingRight + val availableHeight = height - paddingTop - paddingBottom + + val sideLength = min(availableWidth, availableHeight) + + val left = paddingLeft + (availableWidth - sideLength) / 2f + val top = paddingTop + (availableHeight - sideLength) / 2f + + return RectF(left, top, left + sideLength, top + sideLength) + } + + private fun updateShaderMatrix() { + val scale: Float + var dx = 0f + var dy = 0f + + mShaderMatrix.set(null) + + if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { + scale = mDrawableRect.height() / mBitmapHeight.toFloat() + dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f + } else { + scale = mDrawableRect.width() / mBitmapWidth.toFloat() + dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f + } + + mShaderMatrix.setScale(scale, scale) + mShaderMatrix.postTranslate( + (dx + 0.5f).toInt() + mDrawableRect.left, + (dy + 0.5f).toInt() + mDrawableRect.top + ) + + mBitmapShader!!.setLocalMatrix(mShaderMatrix) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isInView = (inTouchableArea(event.x, event.y)) + } + } + return super.onTouchEvent(event) + } + + private fun inTouchableArea(x: Float, y: Float): Boolean { + return (x - mBorderRect.centerX()).toDouble() + .pow(2.0) + (y - mBorderRect.centerY()).toDouble() + .pow(2.0) <= mBorderRadius.toDouble().pow(2.0) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private inner class OutlineProvider : ViewOutlineProvider() { + + override fun getOutline(view: View, outline: Outline) { + val bounds = Rect() + mBorderRect.roundOut(bounds) + outline.setRoundRect(bounds, bounds.width() / 2.0f) + } + + } + + companion object { + private val SCALE_TYPE = ScaleType.CENTER_CROP + private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 + private const val COLOR_DRAWABLE_DIMENSION = 2 + private const val DEFAULT_BORDER_WIDTH = 0 + private const val DEFAULT_BORDER_COLOR = Color.BLACK + private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT + private const val DEFAULT_BORDER_OVERLAY = false + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/image/CoverImageView.kt b/app/src/main/java/com/novel/read/ui/widget/image/CoverImageView.kt new file mode 100644 index 0000000..7e16306 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/image/CoverImageView.kt @@ -0,0 +1,187 @@ +package com.novel.read.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import android.text.TextPaint +import android.util.AttributeSet +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.novel.read.App +import com.novel.read.R +import com.novel.read.constant.PreferKey +import com.novel.read.help.ImageLoader +import com.novel.read.utils.ext.getPrefString + +/** + * 封面 + */ +@Suppress("unused") +class CoverImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : androidx.appcompat.widget.AppCompatImageView( + context, + attrs +) { + internal var width: Float = 0.toFloat() + internal var height: Float = 0.toFloat() + private var nameHeight = 0f + private var authorHeight = 0f + private val namePaint by lazy { + val textPaint = TextPaint() + textPaint.typeface = Typeface.DEFAULT_BOLD + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint.textSkewX = -0.2f + textPaint + } + private val authorPaint by lazy { + val textPaint = TextPaint() + textPaint.typeface = Typeface.DEFAULT + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint.textSkewX = -0.1f + textPaint + } + private var name: String? = null + private var author: String? = null + private var loadFailed = false + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + val measuredHeight = measuredWidth * 7 / 5 + super.onMeasure( + widthMeasureSpec, + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + width = getWidth().toFloat() + height = getHeight().toFloat() + namePaint.textSize = width / 6 + namePaint.strokeWidth = namePaint.textSize / 10 + authorPaint.textSize = width / 9 + authorPaint.strokeWidth = authorPaint.textSize / 10 + val fm = namePaint.fontMetrics + nameHeight = height * 0.5f + (fm.bottom - fm.top) * 0.5f + authorHeight = nameHeight + (fm.bottom - fm.top) * 0.6f + } + + override fun onDraw(canvas: Canvas) { + if (width >= 10 && height > 10) { + @SuppressLint("DrawAllocation") + val path = Path() + //四个圆角 + path.moveTo(10f, 0f) + path.lineTo(width - 10, 0f) + path.quadTo(width, 0f, width, 10f) + path.lineTo(width, height - 10) + path.quadTo(width, height, width - 10, height) + path.lineTo(10f, height) + path.quadTo(0f, height, 0f, height - 10) + path.lineTo(0f, 10f) + path.quadTo(0f, 0f, 10f, 0f) + + canvas.clipPath(path) + } + super.onDraw(canvas) + if (!loadFailed || !showBookName) return + name?.let { + namePaint.color = Color.WHITE + namePaint.style = Paint.Style.STROKE + canvas.drawText(it, width / 2, nameHeight, namePaint) + namePaint.color = Color.RED + namePaint.style = Paint.Style.FILL + canvas.drawText(it, width / 2, nameHeight, namePaint) + } + author?.let { + authorPaint.color = Color.WHITE + authorPaint.style = Paint.Style.STROKE + canvas.drawText(it, width / 2, authorHeight, authorPaint) + authorPaint.color = Color.RED + authorPaint.style = Paint.Style.FILL + canvas.drawText(it, width / 2, authorHeight, authorPaint) + } + } + + fun setHeight(height: Int) { + val width = height * 5 / 7 + minimumWidth = width + } + + private fun setText(name: String?, author: String?) { + this.name = + when { + name == null -> null + name.length > 5 -> name.substring(0, 4) + "…" + else -> name + } + this.author = + when { + author == null -> null + author.length > 8 -> author.substring(0, 7) + "…" + else -> author + } + } + + fun load(path: String?, name: String?, author: String?) { + setText(name, author) + ImageLoader.load(context, path)//Glide自动识别http://,content://和file:// + .placeholder(defaultDrawable) + .error(defaultDrawable) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + loadFailed = true + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + loadFailed = false + return false + } + + }) + .centerCrop() + .into(this) + } + + companion object { + private var showBookName = false + lateinit var defaultDrawable: Drawable + + init { + upDefaultCover() + } + + @SuppressLint("UseCompatLoadingForDrawables") + fun upDefaultCover() { + val path = App.INSTANCE.getPrefString(PreferKey.defaultCover) + var dw = Drawable.createFromPath(path) + if (dw == null) { + showBookName = true + dw = App.INSTANCE.resources.getDrawable(R.drawable.image_cover_default, null) + } else { + showBookName = false + } + defaultDrawable = dw!! + } + + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/image/FilletImageView.kt b/app/src/main/java/com/novel/read/ui/widget/image/FilletImageView.kt new file mode 100644 index 0000000..9717170 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/image/FilletImageView.kt @@ -0,0 +1,99 @@ +package com.novel.read.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Path +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import com.novel.read.R +import com.novel.read.utils.ext.dp +import kotlin.math.max + +class FilletImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + internal var width: Float = 0.toFloat() + internal var height: Float = 0.toFloat() + private var leftTopRadius: Int = 0 + private var rightTopRadius: Int = 0 + private var rightBottomRadius: Int = 0 + private var leftBottomRadius: Int = 0 + + init { + // 读取配置 + val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView) + val defaultRadius = 5.dp + val radius = + array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius) + leftTopRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_left_top_radius, + defaultRadius + ) + rightTopRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_right_top_radius, + defaultRadius + ) + rightBottomRadius = + array.getDimensionPixelOffset( + R.styleable.FilletImageView_right_bottom_radius, + defaultRadius + ) + leftBottomRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_left_bottom_radius, + defaultRadius + ) + + //如果四个角的值没有设置,那么就使用通用的radius的值。 + if (defaultRadius == leftTopRadius) { + leftTopRadius = radius + } + if (defaultRadius == rightTopRadius) { + rightTopRadius = radius + } + if (defaultRadius == rightBottomRadius) { + rightBottomRadius = radius + } + if (defaultRadius == leftBottomRadius) { + leftBottomRadius = radius + } + array.recycle() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + width = getWidth().toFloat() + height = getHeight().toFloat() + } + + override fun onDraw(canvas: Canvas) { + //这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪 + val maxLeft = max(leftTopRadius, leftBottomRadius) + val maxRight = max(rightTopRadius, rightBottomRadius) + val minWidth = maxLeft + maxRight + val maxTop = max(leftTopRadius, rightTopRadius) + val maxBottom = max(leftBottomRadius, rightBottomRadius) + val minHeight = maxTop + maxBottom + if (width >= minWidth && height > minHeight) { + @SuppressLint("DrawAllocation") val path = Path() + //四个角:右上,右下,左下,左上 + path.moveTo(leftTopRadius.toFloat(), 0f) + path.lineTo(width - rightTopRadius, 0f) + path.quadTo(width, 0f, width, rightTopRadius.toFloat()) + + path.lineTo(width, height - rightBottomRadius) + path.quadTo(width, height, width - rightBottomRadius, height) + + path.lineTo(leftBottomRadius.toFloat(), height) + path.quadTo(0f, height, 0f, height - leftBottomRadius) + + path.lineTo(0f, leftTopRadius.toFloat()) + path.quadTo(0f, 0f, leftTopRadius.toFloat(), 0f) + + canvas.clipPath(path) + } + super.onDraw(canvas) + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/image/PhotoView.kt b/app/src/main/java/com/novel/read/ui/widget/image/PhotoView.kt new file mode 100644 index 0000000..3658151 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/image/PhotoView.kt @@ -0,0 +1,1258 @@ +package com.novel.read.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.* +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.ScaleGestureDetector.OnScaleGestureListener +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import android.widget.ImageView +import android.widget.OverScroller +import android.widget.Scroller +import androidx.appcompat.widget.AppCompatImageView +import com.novel.read.R +import com.novel.read.ui.widget.image.photo.Info +import com.novel.read.ui.widget.image.photo.OnRotateListener +import com.novel.read.ui.widget.image.photo.RotateGestureDetector +import kotlin.math.abs +import kotlin.math.roundToInt + +@Suppress("UNUSED_PARAMETER", "unused", "MemberVisibilityCanBePrivate", "PropertyName") +class PhotoView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + val MIN_ROTATE = 35 + val ANIMA_DURING = 340 + val MAX_SCALE = 2.5f + + private var mMinRotate = 0 + var mAnimaDuring = 0 + private var mMaxScale = 0f + + var MAX_OVER_SCROLL = 0 + var MAX_FLING_OVER_SCROLL = 0 + var MAX_OVER_RESISTANCE = 0 + var MAX_ANIM_FROM_WAITE = 500 + + private val mBaseMatrix: Matrix = Matrix() + private val mAnimMatrix: Matrix = Matrix() + private val mSynthesisMatrix: Matrix = Matrix() + private val mTmpMatrix: Matrix = Matrix() + + private val mRotateDetector: RotateGestureDetector + private val mDetector: GestureDetector + private val mScaleDetector: ScaleGestureDetector + private var mClickListener: OnClickListener? = null + + private var mScaleType: ScaleType? = null + + private var hasMultiTouch = false + private var hasDrawable = false + private var isKnowSize = false + private var hasOverTranslate = false + + //缩放 + var isEnable = true + + //旋转 + var isRotateEnable = false + private var isInit = false + private var mAdjustViewBounds = false + + // 当前是否处于放大状态 + private var isZoonUp = false + private var canRotate = false + + private var imgLargeWidth = false + private var imgLargeHeight = false + + private var mRotateFlag = 0f + private var mDegrees = 0f + private var mScale = 1.0f + private var mTranslateX = 0 + private var mTranslateY = 0 + + private var mHalfBaseRectWidth = 0f + private var mHalfBaseRectHeight = 0f + + private val mWidgetRect = RectF() + private val mBaseRect = RectF() + private val mImgRect = RectF() + private val mTmpRect = RectF() + private val mCommonRect = RectF() + + private val mScreenCenter = PointF() + private val mScaleCenter = PointF() + private val mRotateCenter = PointF() + + private val mTranslate: Transform = Transform() + + private var mClip: RectF? = null + private var mFromInfo: Info? = null + private var mInfoTime: Long = 0 + private var mCompleteCallBack: Runnable? = null + + private var mLongClick: OnLongClickListener? = null + + private val mRotateListener = RotateListener() + private val mGestureListener = GestureListener() + private val mScaleListener = ScaleGestureListener() + + init { + super.setScaleType(ScaleType.MATRIX) + if (mScaleType == null) mScaleType = ScaleType.CENTER_INSIDE + mRotateDetector = RotateGestureDetector(mRotateListener) + mDetector = GestureDetector(context, mGestureListener) + mScaleDetector = ScaleGestureDetector(context, mScaleListener) + val density = resources.displayMetrics.density + MAX_OVER_SCROLL = (density * 30).toInt() + MAX_FLING_OVER_SCROLL = (density * 30).toInt() + MAX_OVER_RESISTANCE = (density * 140).toInt() + mMinRotate = MIN_ROTATE + mAnimaDuring = ANIMA_DURING + mMaxScale = MAX_SCALE + } + + /** + * 获取默认的动画持续时间 + */ + fun getDefaultAnimDuring(): Int { + return ANIMA_DURING + } + + override fun setOnClickListener(l: OnClickListener?) { + super.setOnClickListener(l) + mClickListener = l + } + + override fun setScaleType(scaleType: ScaleType) { + if (scaleType == ScaleType.MATRIX) return + if (scaleType != mScaleType) { + mScaleType = scaleType + if (isInit) { + initBase() + } + } + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + mLongClick = l + } + + /** + * 设置动画的插入器 + */ + fun setInterpolator(interpolator: Interpolator?) { + mTranslate.setInterpolator(interpolator) + } + + /** + * 获取动画持续时间 + */ + fun getAnimDuring(): Int { + return mAnimaDuring + } + + /** + * 设置动画的持续时间 + */ + fun setAnimDuring(during: Int) { + mAnimaDuring = during + } + + /** + * 设置最大可以缩放的倍数 + */ + fun setMaxScale(maxScale: Float) { + mMaxScale = maxScale + } + + /** + * 获取最大可以缩放的倍数 + */ + fun getMaxScale(): Float { + return mMaxScale + } + + /** + */ + fun setMaxAnimFromWaiteTime(wait: Int) { + MAX_ANIM_FROM_WAITE = wait + } + + @SuppressLint("UseCompatLoadingForDrawables") + override fun setImageResource(resId: Int) { + var drawable: Drawable? = null + try { + drawable = resources.getDrawable(resId, null) + } catch (e: Exception) { + } + setImageDrawable(drawable) + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + if (drawable == null) { + hasDrawable = false + return + } + if (!hasSize(drawable)) return + if (!hasDrawable) { + hasDrawable = true + } + initBase() + } + + private fun hasSize(d: Drawable): Boolean { + return !((d.intrinsicHeight <= 0 || d.intrinsicWidth <= 0) + && (d.minimumWidth <= 0 || d.minimumHeight <= 0) + && (d.bounds.width() <= 0 || d.bounds.height() <= 0)) + } + + private fun getDrawableWidth(d: Drawable): Int { + var width = d.intrinsicWidth + if (width <= 0) width = d.minimumWidth + if (width <= 0) width = d.bounds.width() + return width + } + + private fun getDrawableHeight(d: Drawable): Int { + var height = d.intrinsicHeight + if (height <= 0) height = d.minimumHeight + if (height <= 0) height = d.bounds.height() + return height + } + + private fun initBase() { + if (!hasDrawable) return + if (!isKnowSize) return + mBaseMatrix.reset() + mAnimMatrix.reset() + isZoonUp = false + val img = drawable + val w = width + val h = height + val imgW = getDrawableWidth(img) + val imgH = getDrawableHeight(img) + mBaseRect[0f, 0f, imgW.toFloat()] = imgH.toFloat() + + // 以图片中心点居中位移 + val tx = (w - imgW) / 2 + val ty = (h - imgH) / 2 + var sx = 1f + var sy = 1f + + // 缩放,默认不超过屏幕大小 + if (imgW > w) { + sx = w.toFloat() / imgW + } + if (imgH > h) { + sy = h.toFloat() / imgH + } + val scale = if (sx < sy) sx else sy + mBaseMatrix.reset() + mBaseMatrix.postTranslate(tx.toFloat(), ty.toFloat()) + mBaseMatrix.postScale(scale, scale, mScreenCenter.x, mScreenCenter.y) + mBaseMatrix.mapRect(mBaseRect) + mHalfBaseRectWidth = mBaseRect.width() / 2 + mHalfBaseRectHeight = mBaseRect.height() / 2 + mScaleCenter.set(mScreenCenter) + mRotateCenter.set(mScaleCenter) + executeTranslate() + when (mScaleType) { + ScaleType.CENTER -> initCenter() + ScaleType.CENTER_CROP -> initCenterCrop() + ScaleType.CENTER_INSIDE -> initCenterInside() + ScaleType.FIT_CENTER -> initFitCenter() + ScaleType.FIT_START -> initFitStart() + ScaleType.FIT_END -> initFitEnd() + ScaleType.FIT_XY -> initFitXY() + else -> { + } + } + isInit = true + mFromInfo?.let { + if (System.currentTimeMillis() - mInfoTime < MAX_ANIM_FROM_WAITE) { + animaFrom(it) + } + } + mFromInfo = null + } + + private fun initCenter() { + if (!hasDrawable) return + if (!isKnowSize) return + val img = drawable + val imgW = getDrawableWidth(img) + val imgH = getDrawableHeight(img) + if (imgW > mWidgetRect.width() || imgH > mWidgetRect.height()) { + val scaleX = imgW / mImgRect.width() + val scaleY = imgH / mImgRect.height() + mScale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initCenterCrop() { + if (mImgRect.width() < mWidgetRect.width() || mImgRect.height() < mWidgetRect.height()) { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mScale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initCenterInside() { + if (mImgRect.width() > mWidgetRect.width() || mImgRect.height() > mWidgetRect.height()) { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mScale = if (scaleX < scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initFitCenter() { + if (mImgRect.width() < mWidgetRect.width()) { + mScale = mWidgetRect.width() / mImgRect.width() + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initFitStart() { + initFitCenter() + val ty = -mImgRect.top + mAnimMatrix.postTranslate(0f, ty) + executeTranslate() + resetBase() + mTranslateY += ty.toInt() + } + + private fun initFitEnd() { + initFitCenter() + val ty = mWidgetRect.bottom - mImgRect.bottom + mTranslateY += ty.toInt() + mAnimMatrix.postTranslate(0f, ty) + executeTranslate() + resetBase() + } + + private fun initFitXY() { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mAnimMatrix.postScale(scaleX, scaleY, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + + private fun resetBase() { + val img = drawable + val imgW = getDrawableWidth(img) + val imgH = getDrawableHeight(img) + mBaseRect[0f, 0f, imgW.toFloat()] = imgH.toFloat() + mBaseMatrix.set(mSynthesisMatrix) + mBaseMatrix.mapRect(mBaseRect) + mHalfBaseRectWidth = mBaseRect.width() / 2 + mHalfBaseRectHeight = mBaseRect.height() / 2 + mScale = 1f + mTranslateX = 0 + mTranslateY = 0 + mAnimMatrix.reset() + } + + private fun executeTranslate() { + mSynthesisMatrix.set(mBaseMatrix) + mSynthesisMatrix.postConcat(mAnimMatrix) + imageMatrix = mSynthesisMatrix + mAnimMatrix.mapRect(mImgRect, mBaseRect) + imgLargeWidth = mImgRect.width() > mWidgetRect.width() + imgLargeHeight = mImgRect.height() > mWidgetRect.height() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (!hasDrawable) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + val d = drawable + val drawableW = getDrawableWidth(d) + val drawableH = getDrawableHeight(d) + val pWidth = MeasureSpec.getSize(widthMeasureSpec) + val pHeight = MeasureSpec.getSize(heightMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + var width: Int + var height: Int + var p = layoutParams + if (p == null) { + p = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + width = if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) { + if (widthMode == MeasureSpec.UNSPECIFIED) { + drawableW + } else { + pWidth + } + } else { + if (widthMode == MeasureSpec.EXACTLY) { + pWidth + } else if (widthMode == MeasureSpec.AT_MOST) { + if (drawableW > pWidth) pWidth else drawableW + } else { + drawableW + } + } + height = if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) { + if (heightMode == MeasureSpec.UNSPECIFIED) { + drawableH + } else { + pHeight + } + } else { + if (heightMode == MeasureSpec.EXACTLY) { + pHeight + } else if (heightMode == MeasureSpec.AT_MOST) { + if (drawableH > pHeight) pHeight else drawableH + } else { + drawableH + } + } + if (mAdjustViewBounds && drawableW.toFloat() / drawableH != width.toFloat() / height) { + val hScale = height.toFloat() / drawableH + val wScale = width.toFloat() / drawableW + val scale = if (hScale < wScale) hScale else wScale + width = + if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) width else (drawableW * scale).toInt() + height = + if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) height else (drawableH * scale).toInt() + } + setMeasuredDimension(width, height) + } + + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + super.setAdjustViewBounds(adjustViewBounds) + mAdjustViewBounds = adjustViewBounds + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidgetRect[0f, 0f, w.toFloat()] = h.toFloat() + mScreenCenter[w / 2.toFloat()] = h / 2.toFloat() + if (!isKnowSize) { + isKnowSize = true + initBase() + } + } + + override fun draw(canvas: Canvas) { + mClip?.let { + canvas.clipRect(it) + mClip = null + } + super.draw(canvas) + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return if (isEnable) { + val action = event.actionMasked + if (event.pointerCount >= 2) hasMultiTouch = true + mDetector.onTouchEvent(event) + if (isRotateEnable) { + mRotateDetector.onTouchEvent(event) + } + mScaleDetector.onTouchEvent(event) + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) onUp() + true + } else { + super.dispatchTouchEvent(event) + } + } + + private fun onUp() { + if (mTranslate.isRunning) return + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + var scale = mScale + if (mScale < 1) { + scale = 1f + mTranslate.withScale(mScale, 1F) + } else if (mScale > mMaxScale) { + scale = mMaxScale + mTranslate.withScale(mScale, mMaxScale) + } + val cx = mImgRect.left + mImgRect.width() / 2 + val cy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[cx] = cy + mRotateCenter[cx] = cy + mTranslateX = 0 + mTranslateY = 0 + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(cx - mHalfBaseRectWidth, cy - mHalfBaseRectHeight) + mTmpMatrix.postScale(scale, scale, cx, cy) + mTmpMatrix.postRotate(mDegrees, cx, cy) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + mTranslate.start() + } + + private fun doTranslateReset(imgRect: RectF) { + var tx = 0 + var ty = 0 + if (imgRect.width() <= mWidgetRect.width()) { + if (!isImageCenterWidth(imgRect)) tx = + (-((mWidgetRect.width() - imgRect.width()) / 2 - imgRect.left)).toInt() + } else { + if (imgRect.left > mWidgetRect.left) { + tx = (imgRect.left - mWidgetRect.left).toInt() + } else if (imgRect.right < mWidgetRect.right) { + tx = (imgRect.right - mWidgetRect.right).toInt() + } + } + if (imgRect.height() <= mWidgetRect.height()) { + if (!isImageCenterHeight(imgRect)) ty = + (-((mWidgetRect.height() - imgRect.height()) / 2 - imgRect.top)).toInt() + } else { + if (imgRect.top > mWidgetRect.top) { + ty = (imgRect.top - mWidgetRect.top).toInt() + } else if (imgRect.bottom < mWidgetRect.bottom) { + ty = (imgRect.bottom - mWidgetRect.bottom).toInt() + } + } + if (tx != 0 || ty != 0) { + if (!mTranslate.mFlingScroller.isFinished) mTranslate.mFlingScroller.abortAnimation() + mTranslate.withTranslate(mTranslateX, mTranslateY, -tx, -ty) + } + } + + private fun isImageCenterHeight(rect: RectF): Boolean { + return abs(rect.top.roundToInt() - (mWidgetRect.height() - rect.height()) / 2) < 1 + } + + private fun isImageCenterWidth(rect: RectF): Boolean { + return abs(rect.left.roundToInt() - (mWidgetRect.width() - rect.width()) / 2) < 1 + } + + private fun resistanceScrollByX( + overScroll: Float, + detalX: Float + ): Float { + return detalX * (abs(abs(overScroll) - MAX_OVER_RESISTANCE) / MAX_OVER_RESISTANCE.toFloat()) + } + + private fun resistanceScrollByY( + overScroll: Float, + detalY: Float + ): Float { + return detalY * (abs(abs(overScroll) - MAX_OVER_RESISTANCE) / MAX_OVER_RESISTANCE.toFloat()) + } + + /** + * 匹配两个Rect的共同部分输出到out,若无共同部分则输出0,0,0,0 + */ + private fun mapRect(r1: RectF, r2: RectF, out: RectF) { + val l: Float = if (r1.left > r2.left) r1.left else r2.left + val r: Float = if (r1.right < r2.right) r1.right else r2.right + if (l > r) { + out[0f, 0f, 0f] = 0f + return + } + val t: Float = if (r1.top > r2.top) r1.top else r2.top + val b: Float = if (r1.bottom < r2.bottom) r1.bottom else r2.bottom + if (t > b) { + out[0f, 0f, 0f] = 0f + return + } + out[l, t, r] = b + } + + private fun checkRect() { + if (!hasOverTranslate) { + mapRect(mWidgetRect, mImgRect, mCommonRect) + } + } + + private val mClickRunnable = Runnable { + mClickListener?.onClick(this) + } + + fun canScrollHorizontallySelf(direction: Float): Boolean { + if (mImgRect.width() <= mWidgetRect.width()) + return false + if (direction < 0 && mImgRect.left.roundToInt() - direction >= mWidgetRect.left) + return false + return !(direction > 0 && mImgRect.right.roundToInt() - direction <= mWidgetRect.right) + } + + fun canScrollVerticallySelf(direction: Float): Boolean { + if (mImgRect.height() <= mWidgetRect.height()) + return false + if (direction < 0 && mImgRect.top.roundToInt() - direction >= mWidgetRect.top) + return false + return !(direction > 0 && mImgRect.bottom.roundToInt() - direction <= mWidgetRect.bottom) + } + + override fun canScrollHorizontally(direction: Int): Boolean { + return if (hasMultiTouch) true else canScrollHorizontallySelf(direction.toFloat()) + } + + override fun canScrollVertically(direction: Int): Boolean { + return if (hasMultiTouch) true else canScrollVerticallySelf(direction.toFloat()) + } + + private inner class InterpolatorProxy : Interpolator { + private var mTarget: Interpolator? + + init { + mTarget = DecelerateInterpolator() + } + + fun setTargetInterpolator(interpolator: Interpolator?) { + mTarget = interpolator + } + + override fun getInterpolation(input: Float): Float { + return mTarget?.getInterpolation(input) ?: input + } + + } + + private inner class Transform : Runnable { + var isRunning = false + var mTranslateScroller: OverScroller + var mFlingScroller: OverScroller + var mScaleScroller: Scroller + var mClipScroller: Scroller + var mRotateScroller: Scroller + var c: ClipCalculate? = null + var mLastFlingX = 0 + var mLastFlingY = 0 + var mLastTranslateX = 0 + var mLastTranslateY = 0 + var mClipRect = RectF() + var mInterpolatorProxy = InterpolatorProxy() + + fun setInterpolator(interpolator: Interpolator?) { + mInterpolatorProxy.setTargetInterpolator(interpolator) + } + + init { + val ctx: Context = context + mTranslateScroller = OverScroller(ctx, mInterpolatorProxy) + mScaleScroller = Scroller(ctx, mInterpolatorProxy) + mFlingScroller = OverScroller(ctx, mInterpolatorProxy) + mClipScroller = Scroller(ctx, mInterpolatorProxy) + mRotateScroller = Scroller(ctx, mInterpolatorProxy) + } + + fun withTranslate(startX: Int, startY: Int, deltaX: Int, deltaY: Int) { + mLastTranslateX = 0 + mLastTranslateY = 0 + mTranslateScroller.startScroll(0, 0, deltaX, deltaY, mAnimaDuring) + } + + fun withScale(form: Float, to: Float) { + mScaleScroller.startScroll( + (form * 10000).toInt(), + 0, + ((to - form) * 10000).toInt(), + 0, + mAnimaDuring + ) + } + + fun withClip( + fromX: Float, + fromY: Float, + deltaX: Float, + deltaY: Float, + d: Int, + c: ClipCalculate? + ) { + mClipScroller.startScroll( + (fromX * 10000).toInt(), + (fromY * 10000).toInt(), + (deltaX * 10000).toInt(), + (deltaY * 10000).toInt(), + d + ) + this.c = c + } + + fun withRotate(fromDegrees: Int, toDegrees: Int) { + mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, mAnimaDuring) + } + + fun withRotate(fromDegrees: Int, toDegrees: Int, during: Int) { + mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, during) + } + + fun withFling(velocityX: Float, velocityY: Float) { + mLastFlingX = if (velocityX < 0) Int.MAX_VALUE else 0 + var distanceX = + (if (velocityX > 0) abs(mImgRect.left) else mImgRect.right - mWidgetRect.right).toInt() + distanceX = if (velocityX < 0) Int.MAX_VALUE - distanceX else distanceX + var minX = if (velocityX < 0) distanceX else 0 + var maxX = if (velocityX < 0) Int.MAX_VALUE else distanceX + val overX = if (velocityX < 0) Int.MAX_VALUE - minX else distanceX + mLastFlingY = if (velocityY < 0) Int.MAX_VALUE else 0 + var distanceY = + (if (velocityY > 0) abs(mImgRect.top) else mImgRect.bottom - mWidgetRect.bottom).toInt() + distanceY = if (velocityY < 0) Int.MAX_VALUE - distanceY else distanceY + var minY = if (velocityY < 0) distanceY else 0 + var maxY = if (velocityY < 0) Int.MAX_VALUE else distanceY + val overY = if (velocityY < 0) Int.MAX_VALUE - minY else distanceY + if (velocityX == 0f) { + maxX = 0 + minX = 0 + } + if (velocityY == 0f) { + maxY = 0 + minY = 0 + } + mFlingScroller.fling( + mLastFlingX, + mLastFlingY, + velocityX.toInt(), + velocityY.toInt(), + minX, + maxX, + minY, + maxY, + if (abs(overX) < MAX_FLING_OVER_SCROLL * 2) 0 else MAX_FLING_OVER_SCROLL, + if (abs(overY) < MAX_FLING_OVER_SCROLL * 2) 0 else MAX_FLING_OVER_SCROLL + ) + } + + fun start() { + isRunning = true + postExecute() + } + + fun stop() { + removeCallbacks(this) + mTranslateScroller.abortAnimation() + mScaleScroller.abortAnimation() + mFlingScroller.abortAnimation() + mRotateScroller.abortAnimation() + isRunning = false + } + + override fun run() { + + // if (!isRuning) return; + var endAnima = true + if (mScaleScroller.computeScrollOffset()) { + mScale = mScaleScroller.currX / 10000f + endAnima = false + } + if (mTranslateScroller.computeScrollOffset()) { + val tx = mTranslateScroller.currX - mLastTranslateX + val ty = mTranslateScroller.currY - mLastTranslateY + mTranslateX += tx + mTranslateY += ty + mLastTranslateX = mTranslateScroller.currX + mLastTranslateY = mTranslateScroller.currY + endAnima = false + } + if (mFlingScroller.computeScrollOffset()) { + val x = mFlingScroller.currX - mLastFlingX + val y = mFlingScroller.currY - mLastFlingY + mLastFlingX = mFlingScroller.currX + mLastFlingY = mFlingScroller.currY + mTranslateX += x + mTranslateY += y + endAnima = false + } + if (mRotateScroller.computeScrollOffset()) { + mDegrees = mRotateScroller.currX.toFloat() + endAnima = false + } + if (mClipScroller.computeScrollOffset() || mClip != null) { + val sx = mClipScroller.currX / 10000f + val sy = mClipScroller.currY / 10000f + mTmpMatrix.setScale( + sx, + sy, + (mImgRect.left + mImgRect.right) / 2, + c!!.calculateTop() + ) + mTmpMatrix.mapRect(mClipRect, mImgRect) + if (sx == 1f) { + mClipRect.left = mWidgetRect.left + mClipRect.right = mWidgetRect.right + } + if (sy == 1f) { + mClipRect.top = mWidgetRect.top + mClipRect.bottom = mWidgetRect.bottom + } + mClip = mClipRect + } + if (!endAnima) { + applyAnima() + postExecute() + } else { + isRunning = false + + // 修复动画结束后边距有些空隙, + var needFix = false + if (imgLargeWidth) { + if (mImgRect.left > 0) { + mTranslateX -= mImgRect.left.toInt() + } else if (mImgRect.right < mWidgetRect.width()) { + mTranslateX -= (mWidgetRect.width() - mImgRect.right).toInt() + } + needFix = true + } + if (imgLargeHeight) { + if (mImgRect.top > 0) { + mTranslateY -= mImgRect.top.toInt() + } else if (mImgRect.bottom < mWidgetRect.height()) { + mTranslateY -= (mWidgetRect.height() - mImgRect.bottom).toInt() + } + needFix = true + } + if (needFix) { + applyAnima() + } + invalidate() + mCompleteCallBack?.let { + it.run() + mCompleteCallBack = null + } + } + } + + private fun applyAnima() { + mAnimMatrix.reset() + mAnimMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mAnimMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mAnimMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mAnimMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mAnimMatrix.postScale(mScale, mScale, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + executeTranslate() + } + + private fun postExecute() { + if (isRunning) post(this) + } + + } + + fun getInfo(): Info { + val rect = RectF() + val p = IntArray(2) + getLocation(this, p) + rect[p[0] + mImgRect.left, p[1] + mImgRect.top, p[0] + mImgRect.right] = + p[1] + mImgRect.bottom + return Info( + rect, + mImgRect, + mWidgetRect, + mBaseRect, + mScreenCenter, + mScale, + mDegrees, + mScaleType + ) + } + + fun getImageViewInfo(imgView: ImageView): Info? { + val p = IntArray(2) + getLocation(imgView, p) + val drawable: Drawable = imgView.drawable + val matrix: Matrix = imgView.imageMatrix + val width = getDrawableWidth(drawable) + val height = getDrawableHeight(drawable) + val imgRect = RectF(0F, 0F, width.toFloat(), height.toFloat()) + matrix.mapRect(imgRect) + val rect = RectF( + p[0] + imgRect.left, + p[1] + imgRect.top, + p[0] + imgRect.right, + p[1] + imgRect.bottom + ) + val widgetRect = RectF(0F, 0F, imgView.width.toFloat(), imgView.height.toFloat()) + val baseRect = RectF(widgetRect) + val screenCenter = PointF(widgetRect.width() / 2, widgetRect.height() / 2) + return Info( + rect, + imgRect, + widgetRect, + baseRect, + screenCenter, + 1F, + 0F, + imgView.scaleType + ) + } + + private fun getLocation(target: View, position: IntArray) { + position[0] += target.left + position[1] += target.top + var viewParent: ViewParent = target.parent + while (viewParent is View) { + val view: View = viewParent + if (view.id == R.id.content) return + position[0] -= view.scrollX + position[1] -= view.scrollY + position[0] += view.left + position[1] += view.top + viewParent = view.parent + } + position[0] = (position[0] + 0.5f).toInt() + position[1] = (position[1] + 0.5f).toInt() + } + + private fun reset() { + mAnimMatrix.reset() + executeTranslate() + mScale = 1f + mTranslateX = 0 + mTranslateY = 0 + } + + interface ClipCalculate { + fun calculateTop(): Float + } + + inner class START : ClipCalculate { + override fun calculateTop(): Float { + return mImgRect.top + } + } + + inner class END : ClipCalculate { + override fun calculateTop(): Float { + return mImgRect.bottom + } + } + + inner class OTHER : ClipCalculate { + override fun calculateTop(): Float { + return (mImgRect.top + mImgRect.bottom) / 2 + } + } + + /** + * 在PhotoView内部还没有图片的时候同样可以调用该方法 + * + * + * 此时并不会播放动画,当给PhotoView设置图片后会自动播放动画。 + * + * + * 若等待时间过长也没有给控件设置图片,则会忽略该动画,若要再次播放动画则需要重新调用该方法 + * (等待的时间默认500毫秒,可以通过setMaxAnimFromWaiteTime(int)设置最大等待时间) + */ + fun animaFrom(info: Info) { + if (isInit) { + reset() + val mine = getInfo() + val scaleX = info.mImgRect.width() / mine.mImgRect.width() + val scaleY = info.mImgRect.height() / mine.mImgRect.height() + val scale = if (scaleX < scaleY) scaleX else scaleY + val ocx = info.mRect.left + info.mRect.width() / 2 + val ocy = info.mRect.top + info.mRect.height() / 2 + val mcx = mine.mRect.left + mine.mRect.width() / 2 + val mcy = mine.mRect.top + mine.mRect.height() / 2 + mAnimMatrix.reset() + // mAnimaMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top); + mAnimMatrix.postTranslate(ocx - mcx, ocy - mcy) + mAnimMatrix.postScale(scale, scale, ocx, ocy) + mAnimMatrix.postRotate(info.mDegrees, ocx, ocy) + executeTranslate() + mScaleCenter[ocx] = ocy + mRotateCenter[ocx] = ocy + mTranslate.withTranslate(0, 0, (-(ocx - mcx)).toInt(), (-(ocy - mcy)).toInt()) + mTranslate.withScale(scale, 1F) + mTranslate.withRotate(info.mDegrees.toInt(), 0) + if (info.mWidgetRect.width() < info.mImgRect.width() || info.mWidgetRect.height() < info.mImgRect.height()) { + var clipX = info.mWidgetRect.width() / info.mImgRect.width() + var clipY = info.mWidgetRect.height() / info.mImgRect.height() + clipX = if (clipX > 1) 1F else clipX + clipY = if (clipY > 1) 1F else clipY + val c = + if (info.mScaleType == ScaleType.FIT_START) START() else if (info.mScaleType == ScaleType.FIT_END) END() else OTHER() + mTranslate.withClip(clipX, clipY, 1 - clipX, 1 - clipY, mAnimaDuring / 3, c) + mTmpMatrix.setScale( + clipX, + clipY, + (mImgRect.left + mImgRect.right) / 2, + c.calculateTop() + ) + mTmpMatrix.mapRect(mTranslate.mClipRect, mImgRect) + mClip = mTranslate.mClipRect + } + mTranslate.start() + } else { + mFromInfo = info + mInfoTime = System.currentTimeMillis() + } + } + + fun animaTo( + info: Info, + completeCallBack: Runnable + ) { + if (isInit) { + mTranslate.stop() + mTranslateX = 0 + mTranslateY = 0 + val tcx = info.mRect.left + info.mRect.width() / 2 + val tcy = info.mRect.top + info.mRect.height() / 2 + mScaleCenter[mImgRect.left + mImgRect.width() / 2] = + mImgRect.top + mImgRect.height() / 2 + mRotateCenter.set(mScaleCenter) + + // 将图片旋转回正常位置,用以计算 + mAnimMatrix.postRotate(-mDegrees, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.mapRect(mImgRect, mBaseRect) + + // 缩放 + val scaleX = info.mImgRect.width() / mBaseRect.width() + val scaleY = info.mImgRect.height() / mBaseRect.height() + val scale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postRotate(mDegrees, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.mapRect(mImgRect, mBaseRect) + mDegrees %= 360 + mTranslate.withTranslate( + 0, + 0, + (tcx - mScaleCenter.x).toInt(), + (tcy - mScaleCenter.y).toInt() + ) + mTranslate.withScale(mScale, scale) + mTranslate.withRotate(mDegrees.toInt(), info.mDegrees.toInt(), mAnimaDuring * 2 / 3) + if (info.mWidgetRect.width() < info.mRect.width() || info.mWidgetRect.height() < info.mRect.height()) { + var clipX = info.mWidgetRect.width() / info.mRect.width() + var clipY = info.mWidgetRect.height() / info.mRect.height() + clipX = if (clipX > 1) 1F else clipX + clipY = if (clipY > 1) 1F else clipY + val cx = clipX + val cy = clipY + val c = + if (info.mScaleType == ScaleType.FIT_START) START() else if (info.mScaleType == ScaleType.FIT_END) END() else OTHER() + postDelayed( + { mTranslate.withClip(1F, 1F, -1 + cx, -1 + cy, mAnimaDuring / 2, c) }, + mAnimaDuring / 2.toLong() + ) + } + mCompleteCallBack = completeCallBack + mTranslate.start() + } + } + + fun rotate(degrees: Float) { + mDegrees += degrees + val centerX = (mWidgetRect.left + mWidgetRect.width() / 2).toInt() + val centerY = (mWidgetRect.top + mWidgetRect.height() / 2).toInt() + mAnimMatrix.postRotate(degrees, centerX.toFloat(), centerY.toFloat()) + executeTranslate() + } + + inner class RotateListener : OnRotateListener { + override fun onRotate( + degrees: Float, + focusX: Float, + focusY: Float + ) { + mRotateFlag += degrees + if (canRotate) { + mDegrees += degrees + mAnimMatrix.postRotate(degrees, focusX, focusY) + } else { + if (abs(mRotateFlag) >= mMinRotate) { + canRotate = true + mRotateFlag = 0f + } + } + } + } + + inner class GestureListener : SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + mLongClick?.onLongClick(this@PhotoView) + } + + override fun onDown(e: MotionEvent): Boolean { + hasOverTranslate = false + hasMultiTouch = false + canRotate = false + removeCallbacks(mClickRunnable) + return false + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (hasMultiTouch) return false + if (!imgLargeWidth && !imgLargeHeight) return false + if (mTranslate.isRunning) return false + var vx = velocityX + var vy = velocityY + if (mImgRect.left.roundToInt() >= mWidgetRect.left + || mImgRect.right.roundToInt() <= mWidgetRect.right + ) { + vx = 0f + } + if (mImgRect.top.roundToInt() >= mWidgetRect.top + || mImgRect.bottom.roundToInt() <= mWidgetRect.bottom + ) { + vy = 0f + } + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + doTranslateReset(mImgRect) + mTranslate.withFling(vx, vy) + mTranslate.start() + // onUp(e2); + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + var x = distanceX + var y = distanceY + if (mTranslate.isRunning) { + mTranslate.stop() + } + if (canScrollHorizontallySelf(x)) { + if (x < 0 && mImgRect.left - x > mWidgetRect.left) + x = mImgRect.left + if (x > 0 && mImgRect.right - x < mWidgetRect.right) + x = mImgRect.right - mWidgetRect.right + mAnimMatrix.postTranslate(-x, 0f) + mTranslateX -= x.toInt() + } else if (imgLargeWidth || hasMultiTouch || hasOverTranslate) { + checkRect() + if (!hasMultiTouch) { + if (x < 0 && mImgRect.left - x > mCommonRect.left) x = + resistanceScrollByX(mImgRect.left - mCommonRect.left, x) + if (x > 0 && mImgRect.right - x < mCommonRect.right) x = + resistanceScrollByX(mImgRect.right - mCommonRect.right, x) + } + mTranslateX -= x.toInt() + mAnimMatrix.postTranslate(-x, 0f) + hasOverTranslate = true + } + if (canScrollVerticallySelf(y)) { + if (y < 0 && mImgRect.top - y > mWidgetRect.top) y = + mImgRect.top + if (y > 0 && mImgRect.bottom - y < mWidgetRect.bottom) y = + mImgRect.bottom - mWidgetRect.bottom + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + } else if (imgLargeHeight || hasOverTranslate || hasMultiTouch) { + checkRect() + if (!hasMultiTouch) { + if (y < 0 && mImgRect.top - y > mCommonRect.top) y = + resistanceScrollByY(mImgRect.top - mCommonRect.top, y) + if (y > 0 && mImgRect.bottom - y < mCommonRect.bottom) y = + resistanceScrollByY(mImgRect.bottom - mCommonRect.bottom, y) + } + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + hasOverTranslate = true + } + executeTranslate() + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + postDelayed(mClickRunnable, 250) + return false + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + mTranslate.stop() + val from: Float + val to: Float + val imgCx = mImgRect.left + mImgRect.width() / 2 + val imgCy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[imgCx] = imgCy + mRotateCenter[imgCx] = imgCy + mTranslateX = 0 + mTranslateY = 0 + if (isZoonUp) { + from = mScale + to = 1f + } else { + from = mScale + to = mMaxScale + mScaleCenter[e.x] = e.y + } + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mTmpMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postScale(to, to, mScaleCenter.x, mScaleCenter.y) + mTmpMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + isZoonUp = !isZoonUp + mTranslate.withScale(from, to) + mTranslate.start() + return false + } + } + + inner class ScaleGestureListener : OnScaleGestureListener { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scaleFactor = detector.scaleFactor + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false + mScale *= scaleFactor + //mScaleCenter.set(detector.getFocusX(), detector.getFocusY()); + mAnimMatrix.postScale( + scaleFactor, + scaleFactor, + detector.focusX, + detector.focusY + ) + executeTranslate() + return true + } + + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/image/photo/Info.kt b/app/src/main/java/com/novel/read/ui/widget/image/photo/Info.kt new file mode 100644 index 0000000..2ddabba --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/image/photo/Info.kt @@ -0,0 +1,49 @@ +package com.novel.read.ui.widget.image.photo + +import android.graphics.PointF + +import android.graphics.RectF +import android.widget.ImageView + + +@Suppress("MemberVisibilityCanBePrivate") +class Info( + rect: RectF, + img: RectF, + widget: RectF, + base: RectF, + screenCenter: PointF, + scale: Float, + degrees: Float, + scaleType: ImageView.ScaleType? +) { + // 内部图片在整个手机界面的位置 + var mRect = RectF() + + // 控件在窗口的位置 + var mImgRect = RectF() + + var mWidgetRect = RectF() + + var mBaseRect = RectF() + + var mScreenCenter = PointF() + + var mScale = 0f + + var mDegrees = 0f + + var mScaleType: ImageView.ScaleType? = null + + init { + mRect.set(rect) + mImgRect.set(img) + mWidgetRect.set(widget) + mScale = scale + mScaleType = scaleType + mDegrees = degrees + mBaseRect.set(base) + mScreenCenter.set(screenCenter) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/image/photo/RotateGestureDetector.kt b/app/src/main/java/com/novel/read/ui/widget/image/photo/RotateGestureDetector.kt new file mode 100644 index 0000000..00fcc53 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/image/photo/RotateGestureDetector.kt @@ -0,0 +1,54 @@ +package com.novel.read.ui.widget.image.photo + +import android.view.MotionEvent +import kotlin.math.abs +import kotlin.math.atan + +class RotateGestureDetector(private val mListener: OnRotateListener) { + + private val MAX_DEGREES_STEP = 120 + + private var mPrevSlope = 0f + private var mCurrSlope = 0f + + private val x1 = 0f + private val y1 = 0f + private val x2 = 0f + private val y2 = 0f + + fun onTouchEvent(event: MotionEvent) { + + when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN, + MotionEvent.ACTION_POINTER_UP -> { + if (event.pointerCount == 2) mPrevSlope = calculateSlope(event) + } + MotionEvent.ACTION_MOVE -> if (event.pointerCount > 1) { + mCurrSlope = calculateSlope(event) + + val currDegrees = Math.toDegrees(atan(mCurrSlope.toDouble())) + val prevDegrees = Math.toDegrees(atan(mPrevSlope.toDouble())) + + val deltaSlope = currDegrees - prevDegrees + + if (abs(deltaSlope) <= MAX_DEGREES_STEP) { + mListener.onRotate(deltaSlope.toFloat(), (x2 + x1) / 2, (y2 + y1) / 2) + } + mPrevSlope = mCurrSlope + } + } + + } + + private fun calculateSlope(event: MotionEvent): Float { + val x1 = event.getX(0) + val y1 = event.getY(0) + val x2 = event.getX(1) + val y2 = event.getY(1) + return (y2 - y1) / (x2 - x1) + } +} + +interface OnRotateListener { + fun onRotate(degrees: Float, focusX: Float, focusY: Float) +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/EditTextPreferenceDialog.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/EditTextPreferenceDialog.kt new file mode 100644 index 0000000..bdb8c0f --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/EditTextPreferenceDialog.kt @@ -0,0 +1,29 @@ +package com.novel.read.ui.widget.prefs + +import android.app.Dialog +import android.os.Bundle +import androidx.preference.EditTextPreferenceDialogFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat +import com.novel.read.lib.ATH + +class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() { + + companion object { + + fun newInstance(key: String): EditTextPreferenceDialog { + val fragment = EditTextPreferenceDialog() + val b = Bundle(1) + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key) + fragment.arguments = b + return fragment + } + + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(ATH.getDialogBackground()) + return dialog + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/ListPreferenceDialog.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/ListPreferenceDialog.kt new file mode 100644 index 0000000..562f582 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/ListPreferenceDialog.kt @@ -0,0 +1,29 @@ +package com.novel.read.ui.widget.prefs + +import android.app.Dialog +import android.os.Bundle +import androidx.preference.ListPreferenceDialogFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat +import com.novel.read.lib.ATH + +class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() { + + companion object { + + fun newInstance(key: String?): ListPreferenceDialog { + val fragment = ListPreferenceDialog() + val b = Bundle(1) + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key) + fragment.arguments = b + return fragment + } + + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(ATH.getDialogBackground()) + return dialog + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/MultiSelectListPreferenceDialog.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/MultiSelectListPreferenceDialog.kt new file mode 100644 index 0000000..ee5f87c --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/MultiSelectListPreferenceDialog.kt @@ -0,0 +1,32 @@ +package com.novel.read.ui.widget.prefs + +import android.app.Dialog +import android.os.Bundle +import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat +import com.novel.read.lib.ATH + +class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() { + + companion object { + + fun newInstance(key: String?): MultiSelectListPreferenceDialog { + val fragment = + MultiSelectListPreferenceDialog() + val b = Bundle(1) + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key) + fragment.arguments = b + return fragment + } + + } + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(ATH.getDialogBackground()) + return dialog + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/NameListPreference.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/NameListPreference.kt new file mode 100644 index 0000000..7b947a9 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/NameListPreference.kt @@ -0,0 +1,47 @@ +package com.novel.read.ui.widget.prefs + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import androidx.preference.ListPreference +import androidx.preference.PreferenceViewHolder +import com.novel.read.R +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* + + +class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { + + private val isBottomBackground: Boolean + + init { + layoutResource = R.layout.view_preference + widgetLayoutResource = R.layout.item_fillet_text + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Preference) + isBottomBackground = typedArray.getBoolean(R.styleable.Preference_isBottomBackground, false) + typedArray.recycle() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + val v = Preference.bindView( + context, + holder, + icon, + title, + summary, + widgetLayoutResource, + R.id.text_view, + isBottomBackground = isBottomBackground + ) + if (v is TextView) { + v.text = entry + if (isBottomBackground) { + val bgColor = context.bottomBackground + val pTextColor = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + v.setTextColor(pTextColor) + } + } + super.onBindViewHolder(holder) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/Preference.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/Preference.kt new file mode 100644 index 0000000..d5f90af --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/Preference.kt @@ -0,0 +1,123 @@ +package com.novel.read.ui.widget.prefs + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.preference.PreferenceViewHolder +import com.novel.read.R +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* +import org.jetbrains.anko.layoutInflater +import org.jetbrains.anko.sdk27.listeners.onLongClick +import kotlin.math.roundToInt + +class Preference(context: Context, attrs: AttributeSet) : + androidx.preference.Preference(context, attrs) { + + var onLongClick: (() -> Unit)? = null + private val isBottomBackground: Boolean + + init { + layoutResource = R.layout.view_preference + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Preference) + isBottomBackground = typedArray.getBoolean(R.styleable.Preference_isBottomBackground, false) + typedArray.recycle() + } + + companion object { + + fun bindView( + context: Context, + it: PreferenceViewHolder?, + icon: Drawable?, + title: CharSequence?, + summary: CharSequence?, + weightLayoutRes: Int? = null, + viewId: Int? = null, + weightWidth: Int = 0, + weightHeight: Int = 0, + isBottomBackground: Boolean = false + ): T? { + if (it == null) return null + val tvTitle = it.findViewById(R.id.preference_title) as TextView + tvTitle.text = title + tvTitle.isVisible = title != null && title.isNotEmpty() + val tvSummary = it.findViewById(R.id.preference_desc) as? TextView + tvSummary?.let { + tvSummary.text = summary + tvSummary.isGone = summary.isNullOrEmpty() + } + if (isBottomBackground && !tvTitle.isInEditMode) { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val pTextColor = context.getPrimaryTextColor(isLight) + tvTitle.setTextColor(pTextColor) + val sTextColor = context.getSecondaryTextColor(isLight) + tvSummary?.setTextColor(sTextColor) + } + val iconView = it.findViewById(R.id.preference_icon) + if (iconView is ImageView) { + iconView.isVisible = icon != null + iconView.setImageDrawable(icon) + iconView.setColorFilter(context.accentColor) + } + + if (weightLayoutRes != null && weightLayoutRes != 0 && viewId != null && viewId != 0) { + val lay = it.findViewById(R.id.preference_widget) + if (lay is FrameLayout) { + var needRequestLayout = false + var v = it.itemView.findViewById(viewId) + if (v == null) { + val inflater: LayoutInflater = context.layoutInflater + val childView = inflater.inflate(weightLayoutRes, null) + lay.removeAllViews() + lay.addView(childView) + lay.isVisible = true + v = lay.findViewById(viewId) + } else + needRequestLayout = true + + if (weightWidth > 0 || weightHeight > 0) { + val lp = lay.layoutParams + if (weightHeight > 0) + lp.height = + (context.resources.displayMetrics.density * weightHeight).roundToInt() + if (weightWidth > 0) + lp.width = + (context.resources.displayMetrics.density * weightWidth).roundToInt() + lay.layoutParams = lp + } else if (needRequestLayout) + v.requestLayout() + + return v + } + } + + return null + } + + } + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + bindView( + context, + holder, + icon, + title, + summary, + isBottomBackground = isBottomBackground + ) + super.onBindViewHolder(holder) + holder?.itemView?.onLongClick { + onLongClick?.invoke() + true + } + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/PreferenceCategory.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/PreferenceCategory.kt new file mode 100644 index 0000000..9a6b190 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/PreferenceCategory.kt @@ -0,0 +1,53 @@ +package com.novel.read.ui.widget.prefs + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceViewHolder +import com.novel.read.R +import com.novel.read.help.AppConfig +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* + + +class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) { + + init { + isPersistent = true + layoutResource = R.layout.view_preference_category + } + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + super.onBindViewHolder(holder) + holder?.let { + val view = it.findViewById(R.id.preference_title) + if (view is TextView) { // && !view.isInEditMode + view.text = title + if (view.isInEditMode) return + view.setBackgroundColor(context.backgroundColor) + view.setTextColor(context.accentColor) + view.isVisible = title != null && title.isNotEmpty() + + val da = it.findViewById(R.id.preference_divider_above) + val dividerColor = if (AppConfig.isNightTheme) { + ColorUtils.shiftColor(context.backgroundColor, 1.05f) + } else { + ColorUtils.shiftColor(context.backgroundColor, 0.95f) + } + if (da is View) { + da.setBackgroundColor(dividerColor) + da.isVisible = it.isDividerAllowedAbove + } + val db = it.findViewById(R.id.preference_divider_below) + if (db is View) { + db.setBackgroundColor(dividerColor) + db.isVisible = it.isDividerAllowedBelow + } + } + } + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/prefs/SwitchPreference.kt b/app/src/main/java/com/novel/read/ui/widget/prefs/SwitchPreference.kt new file mode 100644 index 0000000..228288e --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/prefs/SwitchPreference.kt @@ -0,0 +1,41 @@ +package com.novel.read.ui.widget.prefs + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.SwitchCompat +import androidx.preference.PreferenceViewHolder +import androidx.preference.SwitchPreferenceCompat +import com.novel.read.R +import com.novel.read.lib.ATH +import com.novel.read.utils.ext.accentColor + +class SwitchPreference(context: Context, attrs: AttributeSet) : + SwitchPreferenceCompat(context, attrs) { + + private val isBottomBackground: Boolean + + init { + layoutResource = R.layout.view_preference + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Preference) + isBottomBackground = typedArray.getBoolean(R.styleable.Preference_isBottomBackground, false) + typedArray.recycle() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + val v = Preference.bindView( + context, + holder, + icon, + title, + summary, + widgetLayoutResource, + R.id.switchWidget, + isBottomBackground = isBottomBackground + ) + if (v is SwitchCompat && !v.isInEditMode) { + ATH.setTint(v, context.accentColor) + } + super.onBindViewHolder(holder) + } + +} diff --git a/app/src/main/java/com/novel/read/ui/widget/recycler/ItemTouchCallback.kt b/app/src/main/java/com/novel/read/ui/widget/recycler/ItemTouchCallback.kt new file mode 100644 index 0000000..7bea5de --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/recycler/ItemTouchCallback.kt @@ -0,0 +1,133 @@ +package com.novel.read.ui.widget.recycler + + +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.viewpager.widget.ViewPager + +@Suppress("MemberVisibilityCanBePrivate") +class ItemTouchCallback(private val callback: Callback) : ItemTouchHelper.Callback() { + + private var swipeRefreshLayout: SwipeRefreshLayout? = null + private var viewPager: ViewPager? = null + + /** + * 是否可以拖拽 + */ + var isCanDrag = false + + /** + * 是否可以被滑动 + */ + var isCanSwipe = false + + /** + * 当Item被长按的时候是否可以被拖拽 + */ + override fun isLongPressDragEnabled(): Boolean { + return isCanDrag + } + + /** + * Item是否可以被滑动(H:左右滑动,V:上下滑动) + */ + override fun isItemViewSwipeEnabled(): Boolean { + return isCanSwipe + } + + /** + * 当用户拖拽或者滑动Item的时候需要我们告诉系统滑动或者拖拽的方向 + */ + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + val layoutManager = recyclerView.layoutManager + if (layoutManager is GridLayoutManager) {// GridLayoutManager + // flag如果值是0,相当于这个功能被关闭 + val dragFlag = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN + val swipeFlag = 0 + // create make + return makeMovementFlags(dragFlag, swipeFlag) + } else if (layoutManager is LinearLayoutManager) {// linearLayoutManager + val linearLayoutManager = layoutManager as LinearLayoutManager? + val orientation = linearLayoutManager!!.orientation + + var dragFlag = 0 + var swipeFlag = 0 + + // 为了方便理解,相当于分为横着的ListView和竖着的ListView + if (orientation == LinearLayoutManager.HORIZONTAL) {// 如果是横向的布局 + swipeFlag = ItemTouchHelper.UP or ItemTouchHelper.DOWN + dragFlag = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + } else if (orientation == LinearLayoutManager.VERTICAL) {// 如果是竖向的布局,相当于ListView + dragFlag = ItemTouchHelper.UP or ItemTouchHelper.DOWN + swipeFlag = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + } + return makeMovementFlags(dragFlag, swipeFlag) + } + return 0 + } + + /** + * 当Item被拖拽的时候被回调 + * + * @param recyclerView recyclerView + * @param srcViewHolder 拖拽的ViewHolder + * @param targetViewHolder 目的地的viewHolder + */ + override fun onMove( + recyclerView: RecyclerView, + srcViewHolder: RecyclerView.ViewHolder, + targetViewHolder: RecyclerView.ViewHolder + ): Boolean { + return callback.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition) + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + callback.onSwiped(viewHolder.adapterPosition) + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + val swiping = actionState == ItemTouchHelper.ACTION_STATE_DRAG + swipeRefreshLayout?.isEnabled = !swiping + viewPager?.requestDisallowInterceptTouchEvent(swiping) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + callback.onClearView(recyclerView, viewHolder) + } + + interface Callback { + + /** + * 当某个Item被滑动删除的时候 + * + * @param adapterPosition item的position + */ + fun onSwiped(adapterPosition: Int) { + + } + + /** + * 当两个Item位置互换的时候被回调 + * + * @param srcPosition 拖拽的item的position + * @param targetPosition 目的地的Item的position + * @return 开发者处理了操作应该返回true,开发者没有处理就返回false + */ + fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + return true + } + + /** + * 手指松开 + */ + fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + + } + + } +} diff --git a/app/src/main/java/com/novel/read/ui/widget/scroller/FastScrollRecyclerView.kt b/app/src/main/java/com/novel/read/ui/widget/scroller/FastScrollRecyclerView.kt new file mode 100644 index 0000000..5aca7c6 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/scroller/FastScrollRecyclerView.kt @@ -0,0 +1,165 @@ +package com.novel.read.ui.widget.scroller + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.RecyclerView +import com.novel.read.R + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class FastScrollRecyclerView : RecyclerView { + + private lateinit var mFastScroller: FastScroller + + constructor(context: Context) : super(context) { + layout(context, null) + layoutParams = + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } + + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int = 0 + ) : super(context, attrs, defStyleAttr) { + layout(context, attrs) + } + + private fun layout(context: Context, attrs: AttributeSet?) { + mFastScroller = FastScroller(context, attrs) + mFastScroller.id = R.id.fast_scroller + } + + override fun setAdapter(adapter: Adapter<*>?) { + super.setAdapter(adapter) + if (adapter is FastScroller.SectionIndexer) { + setSectionIndexer(adapter as FastScroller.SectionIndexer?) + } else if (adapter == null) { + setSectionIndexer(null) + } + } + + + override fun setVisibility(visibility: Int) { + super.setVisibility(visibility) + mFastScroller.visibility = visibility + } + + + /** + * Set the [FastScroller.SectionIndexer] for the [FastScroller]. + * + * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller + */ + fun setSectionIndexer(sectionIndexer: FastScroller.SectionIndexer?) { + mFastScroller.setSectionIndexer(sectionIndexer) + } + + + /** + * Set the enabled state of fast scrolling. + * + * @param enabled True to enable fast scrolling, false otherwise + */ + fun setFastScrollEnabled(enabled: Boolean) { + mFastScroller.isEnabled = enabled + } + + + /** + * Hide the scrollbar when not scrolling. + * + * @param hideScrollbar True to hide the scrollbar, false to show + */ + fun setHideScrollbar(hideScrollbar: Boolean) { + mFastScroller.setFadeScrollbar(hideScrollbar) + } + + /** + * Display a scroll track while scrolling. + * + * @param visible True to show scroll track, false to hide + */ + fun setTrackVisible(visible: Boolean) { + mFastScroller.setTrackVisible(visible) + } + + /** + * Set the color of the scroll track. + * + * @param color The color for the scroll track + */ + fun setTrackColor(@ColorInt color: Int) { + mFastScroller.setTrackColor(color) + } + + + /** + * Set the color for the scroll handle. + * + * @param color The color for the scroll handle + */ + fun setHandleColor(@ColorInt color: Int) { + mFastScroller.setHandleColor(color) + } + + + /** + * Show the section bubble while scrolling. + * + * @param visible True to show the bubble, false to hide + */ + fun setBubbleVisible(visible: Boolean) { + mFastScroller.setBubbleVisible(visible) + } + + + /** + * Set the background color of the index bubble. + * + * @param color The background color for the index bubble + */ + fun setBubbleColor(@ColorInt color: Int) { + mFastScroller.setBubbleColor(color) + } + + + /** + * Set the text color of the index bubble. + * + * @param color The text color for the index bubble + */ + fun setBubbleTextColor(@ColorInt color: Int) { + mFastScroller.setBubbleTextColor(color) + } + + + /** + * Set the fast scroll state change listener. + * + * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events + */ + fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) { + mFastScroller.setFastScrollStateChangeListener(fastScrollStateChangeListener) + } + + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mFastScroller.attachRecyclerView(this) + val parent = parent + if (parent is ViewGroup) { + parent.addView(mFastScroller) + mFastScroller.setLayoutParams(parent) + } + } + + + override fun onDetachedFromWindow() { + mFastScroller.detachRecyclerView() + super.onDetachedFromWindow() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/scroller/FastScrollStateChangeListener.kt b/app/src/main/java/com/novel/read/ui/widget/scroller/FastScrollStateChangeListener.kt new file mode 100644 index 0000000..8302a84 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/scroller/FastScrollStateChangeListener.kt @@ -0,0 +1,15 @@ +package com.novel.read.ui.widget.scroller + + +interface FastScrollStateChangeListener { + + /** + * Called when fast scrolling begins + */ + fun onFastScrollStart(fastScroller: FastScroller) + + /** + * Called when fast scrolling ends + */ + fun onFastScrollStop(fastScroller: FastScroller) +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/ui/widget/scroller/FastScroller.kt b/app/src/main/java/com/novel/read/ui/widget/scroller/FastScroller.kt new file mode 100644 index 0000000..d237253 --- /dev/null +++ b/app/src/main/java/com/novel/read/ui/widget/scroller/FastScroller.kt @@ -0,0 +1,519 @@ +package com.novel.read.ui.widget.scroller + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewPropertyAnimator +import android.widget.* +import androidx.annotation.ColorInt +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.GravityCompat +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.novel.read.R +import com.novel.read.utils.ColorUtils +import com.novel.read.utils.ext.* +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + + +@Suppress("SameParameterValue") +class FastScroller : LinearLayout { + @ColorInt + private var mBubbleColor: Int = 0 + + @ColorInt + private var mHandleColor: Int = 0 + private var mBubbleHeight: Int = 0 + private var mHandleHeight: Int = 0 + private var mViewHeight: Int = 0 + private var mFadeScrollbar: Boolean = false + private var mShowBubble: Boolean = false + private var mSectionIndexer: SectionIndexer? = null + private var mScrollbarAnimator: ViewPropertyAnimator? = null + private var mBubbleAnimator: ViewPropertyAnimator? = null + private var mRecyclerView: RecyclerView? = null + private lateinit var mBubbleView: TextView + private lateinit var mHandleView: ImageView + private lateinit var mTrackView: ImageView + private lateinit var mScrollbar: View + private var mBubbleImage: Drawable? = null + private var mHandleImage: Drawable? = null + private var mTrackImage: Drawable? = null + private var mFastScrollStateChangeListener: FastScrollStateChangeListener? = null + private val mScrollbarHider = Runnable { this.hideScrollbar() } + + private val mScrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (!mHandleView.isSelected && isEnabled) { + setViewPositions(getScrollProportion(recyclerView)) + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (isEnabled) { + when (newState) { + RecyclerView.SCROLL_STATE_DRAGGING -> { + handler.removeCallbacks(mScrollbarHider) + cancelAnimation(mScrollbarAnimator) + if (!isViewVisible(mScrollbar)) { + showScrollbar() + } + } + RecyclerView.SCROLL_STATE_IDLE -> if (mFadeScrollbar && !mHandleView.isSelected) { + handler.postDelayed(mScrollbarHider, sScrollbarHideDelay.toLong()) + } + } + } + } + } + + constructor(context: Context) : super(context) { + layout(context, null) + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT) + } + + @JvmOverloads + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) { + layout(context, attrs) + layoutParams = generateLayoutParams(attrs) + } + + override fun setLayoutParams(params: ViewGroup.LayoutParams) { + params.width = LayoutParams.WRAP_CONTENT + super.setLayoutParams(params) + } + + fun setLayoutParams(viewGroup: ViewGroup) { + @IdRes val recyclerViewId = mRecyclerView?.id ?: View.NO_ID + val marginTop = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top) + val marginBottom = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom) + require(recyclerViewId != View.NO_ID) { "RecyclerView must have a view ID" } + when (viewGroup) { + is ConstraintLayout -> { + val constraintSet = ConstraintSet() + @IdRes val layoutId = id + constraintSet.clone(viewGroup) + constraintSet.connect(layoutId, ConstraintSet.TOP, recyclerViewId, ConstraintSet.TOP) + constraintSet.connect(layoutId, ConstraintSet.BOTTOM, recyclerViewId, ConstraintSet.BOTTOM) + constraintSet.connect(layoutId, ConstraintSet.END, recyclerViewId, ConstraintSet.END) + constraintSet.applyTo(viewGroup) + val layoutParams = layoutParams as ConstraintLayout.LayoutParams + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + is CoordinatorLayout -> { + val layoutParams = layoutParams as CoordinatorLayout.LayoutParams + layoutParams.anchorId = recyclerViewId + layoutParams.anchorGravity = GravityCompat.END + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + is FrameLayout -> { + val layoutParams = layoutParams as FrameLayout.LayoutParams + layoutParams.gravity = GravityCompat.END + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + is RelativeLayout -> { + val layoutParams = layoutParams as RelativeLayout.LayoutParams + val endRule = RelativeLayout.ALIGN_END + layoutParams.addRule(RelativeLayout.ALIGN_TOP, recyclerViewId) + layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId) + layoutParams.addRule(endRule, recyclerViewId) + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout") + } + updateViewHeights() + } + + fun setSectionIndexer(sectionIndexer: SectionIndexer?) { + mSectionIndexer = sectionIndexer + } + + fun attachRecyclerView(recyclerView: RecyclerView) { + mRecyclerView = recyclerView + if (mRecyclerView != null) { + mRecyclerView!!.addOnScrollListener(mScrollListener) + post { + // set initial positions for bubble and handle + setViewPositions(getScrollProportion(mRecyclerView)) + } + } + } + + fun detachRecyclerView() { + if (mRecyclerView != null) { + mRecyclerView!!.removeOnScrollListener(mScrollListener) + mRecyclerView = null + } + } + + /** + * Hide the scrollbar when not scrolling. + * + * @param fadeScrollbar True to hide the scrollbar, false to show + */ + fun setFadeScrollbar(fadeScrollbar: Boolean) { + mFadeScrollbar = fadeScrollbar + mScrollbar.visibility = if (fadeScrollbar) View.INVISIBLE else View.VISIBLE + } + + /** + * Show the section bubble while scrolling. + * + * @param visible True to show the bubble, false to hide + */ + fun setBubbleVisible(visible: Boolean) { + mShowBubble = visible + } + + /** + * Display a scroll track while scrolling. + * + * @param visible True to show scroll track, false to hide + */ + fun setTrackVisible(visible: Boolean) { + mTrackView.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + + /** + * Set the color of the scroll track. + * + * @param color The color for the scroll track + */ + fun setTrackColor(@ColorInt color: Int) { + if (mTrackImage == null) { + val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_track) + if (drawable != null) { + mTrackImage = DrawableCompat.wrap(drawable) + } + } + DrawableCompat.setTint(mTrackImage!!, color) + mTrackView.setImageDrawable(mTrackImage) + } + + /** + * Set the color for the scroll handle. + * + * @param color The color for the scroll handle + */ + fun setHandleColor(@ColorInt color: Int) { + mHandleColor = color + if (mHandleImage == null) { + val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_handle) + if (drawable != null) { + mHandleImage = DrawableCompat.wrap(drawable) + } + } + DrawableCompat.setTint(mHandleImage!!, mHandleColor) + mHandleView.setImageDrawable(mHandleImage) + } + + /** + * Set the background color of the index bubble. + * + * @param color The background color for the index bubble + */ + fun setBubbleColor(@ColorInt color: Int) { + mBubbleColor = color + if (mBubbleImage == null) { + val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_bubble) + if (drawable != null) { + mBubbleImage = DrawableCompat.wrap(drawable) + } + } + DrawableCompat.setTint(mBubbleImage!!, mBubbleColor) + mBubbleView.background = mBubbleImage + } + + /** + * Set the text color of the index bubble. + * + * @param color The text color for the index bubble + */ + fun setBubbleTextColor(@ColorInt color: Int) { + mBubbleView.setTextColor(color) + } + + /** + * Set the fast scroll state change listener. + * + * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events + */ + fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) { + mFastScrollStateChangeListener = fastScrollStateChangeListener + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + visibility = if (enabled) View.VISIBLE else View.INVISIBLE + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (event.x < mHandleView.x - ViewCompat.getPaddingStart(mHandleView)) { + return false + } + requestDisallowInterceptTouchEvent(true) + setHandleSelected(true) + handler.removeCallbacks(mScrollbarHider) + cancelAnimation(mScrollbarAnimator) + cancelAnimation(mBubbleAnimator) + if (!isViewVisible(mScrollbar)) { + showScrollbar() + } + if (mShowBubble && mSectionIndexer != null) { + showBubble() + } + if (mFastScrollStateChangeListener != null) { + mFastScrollStateChangeListener!!.onFastScrollStart(this) + } + val y = event.y + setViewPositions(y) + setRecyclerViewPosition(y) + return true + } + MotionEvent.ACTION_MOVE -> { + val y = event.y + setViewPositions(y) + setRecyclerViewPosition(y) + return true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + requestDisallowInterceptTouchEvent(false) + setHandleSelected(false) + if (mFadeScrollbar) { + handler.postDelayed(mScrollbarHider, sScrollbarHideDelay.toLong()) + } + hideBubble() + if (mFastScrollStateChangeListener != null) { + mFastScrollStateChangeListener!!.onFastScrollStop(this) + } + return true + } + } + return super.onTouchEvent(event) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mViewHeight = h + } + + private fun setRecyclerViewPosition(y: Float) { + if (mRecyclerView != null && mRecyclerView!!.adapter != null) { + val itemCount = mRecyclerView!!.adapter!!.itemCount + val proportion: Float = when { + mHandleView.y == 0f -> 0f + mHandleView.y + mHandleHeight >= mViewHeight - sTrackSnapRange -> 1f + else -> y / mViewHeight.toFloat() + } + var scrolledItemCount = (proportion * itemCount).roundToInt() + if (isLayoutReversed(mRecyclerView!!.layoutManager!!)) { + scrolledItemCount = itemCount - scrolledItemCount + } + val targetPos = getValueInRange(0, itemCount - 1, scrolledItemCount) + mRecyclerView!!.layoutManager!!.scrollToPosition(targetPos) + if (mShowBubble && mSectionIndexer != null) { + mBubbleView.text = mSectionIndexer!!.getSectionText(targetPos) + } + } + } + + private fun getScrollProportion(recyclerView: RecyclerView?): Float { + if (recyclerView == null) { + return 0f + } + val verticalScrollOffset = recyclerView.computeVerticalScrollOffset() + val verticalScrollRange = recyclerView.computeVerticalScrollRange() + val rangeDiff = (verticalScrollRange - mViewHeight).toFloat() + val proportion = verticalScrollOffset.toFloat() / if (rangeDiff > 0) rangeDiff else 1f + return mViewHeight * proportion + } + + private fun getValueInRange(min: Int, max: Int, value: Int): Int { + val minimum = max(min, value) + return min(minimum, max) + } + + private fun setViewPositions(y: Float) { + mBubbleHeight = mBubbleView.height + mHandleHeight = mHandleView.height + val bubbleY = getValueInRange(0, mViewHeight - mBubbleHeight - mHandleHeight / 2, (y - mBubbleHeight).toInt()) + val handleY = getValueInRange(0, mViewHeight - mHandleHeight, (y - mHandleHeight / 2).toInt()) + if (mShowBubble) { + mBubbleView.y = bubbleY.toFloat() + } + mHandleView.y = handleY.toFloat() + } + + private fun updateViewHeights() { + val measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + mBubbleView.measure(measureSpec, measureSpec) + mBubbleHeight = mBubbleView.measuredHeight + mHandleView.measure(measureSpec, measureSpec) + mHandleHeight = mHandleView.measuredHeight + } + + private fun isLayoutReversed(layoutManager: RecyclerView.LayoutManager): Boolean { + if (layoutManager is LinearLayoutManager) { + return layoutManager.reverseLayout + } else if (layoutManager is StaggeredGridLayoutManager) { + return layoutManager.reverseLayout + } + return false + } + + private fun isViewVisible(view: View?): Boolean { + return view != null && view.visibility == View.VISIBLE + } + + private fun cancelAnimation(animator: ViewPropertyAnimator?) { + animator?.cancel() + } + + private fun showBubble() { + if (!isViewVisible(mBubbleView)) { + mBubbleView.visibility = View.VISIBLE + mBubbleAnimator = mBubbleView.animate().alpha(1f) + .setDuration(sBubbleAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + + // adapter required for new alpha value to stick + }) + } + } + + private fun hideBubble() { + if (isViewVisible(mBubbleView)) { + mBubbleAnimator = mBubbleView.animate().alpha(0f) + .setDuration(sBubbleAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + mBubbleView.visibility = View.INVISIBLE + mBubbleAnimator = null + } + + override fun onAnimationCancel(animation: Animator) { + super.onAnimationCancel(animation) + mBubbleView.visibility = View.INVISIBLE + mBubbleAnimator = null + } + }) + } + } + + private fun showScrollbar() { + mRecyclerView?.let { mRecyclerView -> + if (mRecyclerView.computeVerticalScrollRange() - mViewHeight > 0) { + val transX = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat() + mScrollbar.translationX = transX + mScrollbar.visibility = View.VISIBLE + mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f) + .setDuration(sScrollbarAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + + // adapter required for new alpha value to stick + }) + } + } + } + + private fun hideScrollbar() { + val transX = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat() + mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f) + .setDuration(sScrollbarAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + mScrollbar.visibility = View.INVISIBLE + mScrollbarAnimator = null + } + + override fun onAnimationCancel(animation: Animator) { + super.onAnimationCancel(animation) + mScrollbar.visibility = View.INVISIBLE + mScrollbarAnimator = null + } + }) + } + + private fun setHandleSelected(selected: Boolean) { + mHandleView.isSelected = selected + DrawableCompat.setTint(mHandleImage!!, if (selected) mBubbleColor else mHandleColor) + } + + private fun layout(context: Context, attrs: AttributeSet?) { + View.inflate(context, R.layout.view_fastscroller, this) + clipChildren = false + orientation = HORIZONTAL + mBubbleView = findViewById(R.id.fastscroll_bubble) + mHandleView = findViewById(R.id.fastscroll_handle) + mTrackView = findViewById(R.id.fastscroll_track) + mScrollbar = findViewById(R.id.fastscroll_scrollbar) + @ColorInt var bubbleColor = ColorUtils.adjustAlpha(context.accentColor, 0.8f) + @ColorInt var handleColor = context.accentColor + @ColorInt var trackColor = context.getCompatColor(R.color.transparent30) + @ColorInt var textColor = + if (ColorUtils.isColorLight(bubbleColor)) Color.BLACK else Color.WHITE + var fadeScrollbar = true + var showBubble = false + var showTrack = true + if (attrs != null) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0) + try { + bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor) + handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor) + trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor) + textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor) + fadeScrollbar = + typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar) + showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble) + showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack) + } finally { + typedArray.recycle() + } + } + setTrackColor(trackColor) + setHandleColor(handleColor) + setBubbleColor(bubbleColor) + setBubbleTextColor(textColor) + setFadeScrollbar(fadeScrollbar) + setBubbleVisible(showBubble) + setTrackVisible(showTrack) + } + + interface SectionIndexer { + fun getSectionText(position: Int): String + } + + companion object { + private const val sBubbleAnimDuration = 100 + private const val sScrollbarAnimDuration = 300 + private const val sScrollbarHideDelay = 1000 + private const val sTrackSnapRange = 5 + } + +} diff --git a/app/src/main/java/com/novel/read/user/UserManager.kt b/app/src/main/java/com/novel/read/user/UserManager.kt new file mode 100644 index 0000000..f00860a --- /dev/null +++ b/app/src/main/java/com/novel/read/user/UserManager.kt @@ -0,0 +1,5 @@ +package com.novel.read.user + +object UserManager { + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/user/VipHelper.kt b/app/src/main/java/com/novel/read/user/VipHelper.kt new file mode 100644 index 0000000..1dc9170 --- /dev/null +++ b/app/src/main/java/com/novel/read/user/VipHelper.kt @@ -0,0 +1,28 @@ +package com.novel.read.user + +import com.novel.read.App +import com.novel.read.data.db.entity.User + +object VipHelper { + + var user: User? = null + + fun notifyUser() { + user = App.db.getUserDao().getUser() + } + + fun showAd(): Boolean { + return false +// return if (user == null) { +// true +// } else { +// !isVip(user) +// } + } + + private fun isVip(user: User?): Boolean { + val currentTime = System.currentTimeMillis() + return user?.vipStatus == 1 && currentTime > user.vipStartTime!! && currentTime < user.vipEndTime!! + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/AnimationUtilsSupport.kt b/app/src/main/java/com/novel/read/utils/AnimationUtilsSupport.kt new file mode 100644 index 0000000..b688ea0 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/AnimationUtilsSupport.kt @@ -0,0 +1,17 @@ +package com.novel.read.utils + +import android.content.Context +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import androidx.annotation.AnimRes +import com.novel.read.help.AppConfig + +object AnimationUtilsSupport { + fun loadAnimation(context: Context, @AnimRes id: Int): Animation { + val animation = AnimationUtils.loadAnimation(context, id) + if (AppConfig.isEInkMode) { + animation.duration = 0 + } + return animation + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/BitmapUtils.kt b/app/src/main/java/com/novel/read/utils/BitmapUtils.kt new file mode 100644 index 0000000..72e566a --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/BitmapUtils.kt @@ -0,0 +1,286 @@ +package com.novel.read.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Bitmap.Config +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicBlur +import android.view.View +import com.novel.read.App +import java.io.IOException +import kotlin.jvm.Throws +import kotlin.math.* + + +@Suppress("unused", "WeakerAccess") +object BitmapUtils { + + /** + * 从path中获取图片信息,在通过BitmapFactory.decodeFile(String path)方法将突破转成Bitmap时, + * 遇到大一些的图片,我们经常会遇到OOM(Out Of Memory)的问题。所以用到了我们上面提到的BitmapFactory.Options这个类。 + * + * @param path 文件路径 + * @param width 想要显示的图片的宽度 + * @param height 想要显示的图片的高度 + * @return + */ + fun decodeBitmap(path: String, width: Int, height: Int): Bitmap { + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeFile(path, op) //获取尺寸信息 + //获取比例大小 + val wRatio = ceil((op.outWidth / width).toDouble()).toInt() + val hRatio = ceil((op.outHeight / height).toDouble()).toInt() + //如果超出指定大小,则缩小相应的比例 + if (wRatio > 1 && hRatio > 1) { + if (wRatio > hRatio) { + op.inSampleSize = wRatio + } else { + op.inSampleSize = hRatio + } + } + op.inJustDecodeBounds = false + return BitmapFactory.decodeFile(path, op) + } + + /** 从path中获取Bitmap图片 + * @param path 图片路径 + * @return + */ + fun decodeBitmap(path: String): Bitmap { + val opts = BitmapFactory.Options() + opts.inJustDecodeBounds = true + BitmapFactory.decodeFile(path, opts) + opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128) + opts.inJustDecodeBounds = false + + return BitmapFactory.decodeFile(path, opts) + } + + /** + * 以最省内存的方式读取本地资源的图片 + * @param context 设备上下文 + * @param resId 资源ID + * @return + */ + fun decodeBitmap(context: Context, resId: Int): Bitmap? { + val opt = BitmapFactory.Options() + opt.inPreferredConfig = Config.RGB_565 + //获取资源图片 + val `is` = context.resources.openRawResource(resId) + return BitmapFactory.decodeStream(`is`, null, opt) + } + + /** + * @param context 设备上下文 + * @param resId 资源ID + * @param width + * @param height + * @return + */ + fun decodeBitmap(context: Context, resId: Int, width: Int, height: Int): Bitmap? { + + var inputStream = context.resources.openRawResource(resId) + + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeStream(inputStream, null, op) //获取尺寸信息 + //获取比例大小 + val wRatio = ceil((op.outWidth / width).toDouble()).toInt() + val hRatio = ceil((op.outHeight / height).toDouble()).toInt() + //如果超出指定大小,则缩小相应的比例 + if (wRatio > 1 && hRatio > 1) { + if (wRatio > hRatio) { + op.inSampleSize = wRatio + } else { + op.inSampleSize = hRatio + } + } + inputStream = context.resources.openRawResource(resId) + op.inJustDecodeBounds = false + return BitmapFactory.decodeStream(inputStream, null, op) + } + + /** + * @param context 设备上下文 + * @param fileNameInAssets Assets里面文件的名称 + * @param width 图片的宽度 + * @param height 图片的高度 + * @return Bitmap + * @throws IOException + */ + @Throws(IOException::class) + fun decodeAssetsBitmap( + context: Context, + fileNameInAssets: String, + width: Int, + height: Int + ): Bitmap? { + var inputStream = context.assets.open(fileNameInAssets) + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeStream(inputStream, null, op) //获取尺寸信息 + //获取比例大小 + val wRatio = ceil((op.outWidth / width).toDouble()).toInt() + val hRatio = ceil((op.outHeight / height).toDouble()).toInt() + //如果超出指定大小,则缩小相应的比例 + if (wRatio > 1 && hRatio > 1) { + if (wRatio > hRatio) { + op.inSampleSize = wRatio + } else { + op.inSampleSize = hRatio + } + } + inputStream = context.assets.open(fileNameInAssets) + op.inJustDecodeBounds = false + return BitmapFactory.decodeStream(inputStream, null, op) + } + + + //图片不被压缩 + fun convertViewToBitmap(view: View, bitmapWidth: Int, bitmapHeight: Int): Bitmap { + val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Config.ARGB_8888) + view.draw(Canvas(bitmap)) + return bitmap + } + + + /** + * @param options + * @param minSideLength + * @param maxNumOfPixels + * @return + * 设置恰当的inSampleSize是解决该问题的关键之一。BitmapFactory.Options提供了另一个成员inJustDecodeBounds。 + * 设置inJustDecodeBounds为true后,decodeFile并不分配空间,但可计算出原始图片的长度和宽度,即opts.width和opts.height。 + * 有了这两个参数,再通过一定的算法,即可得到一个恰当的inSampleSize。 + * 查看Android源码,Android提供了下面这种动态计算的方法。 + */ + fun computeSampleSize( + options: BitmapFactory.Options, + minSideLength: Int, + maxNumOfPixels: Int + ): Int { + val initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels) + var roundedSize: Int + if (initialSize <= 8) { + roundedSize = 1 + while (roundedSize < initialSize) { + roundedSize = roundedSize shl 1 + } + } else { + roundedSize = (initialSize + 7) / 8 * 8 + } + return roundedSize + } + + + private fun computeInitialSampleSize( + options: BitmapFactory.Options, + minSideLength: Int, + maxNumOfPixels: Int + ): Int { + + val w = options.outWidth.toDouble() + val h = options.outHeight.toDouble() + + val lowerBound = when (maxNumOfPixels) { + -1 -> 1 + else -> ceil(sqrt(w * h / maxNumOfPixels)).toInt() + } + + val upperBound = when (minSideLength) { + -1 -> 128 + else -> min( + floor(w / minSideLength), + floor(h / minSideLength) + ).toInt() + } + + if (upperBound < lowerBound) { + // return the larger one when there is no overlapping zone. + return lowerBound + } + + return when { + maxNumOfPixels == -1 && minSideLength == -1 -> { + 1 + } + minSideLength == -1 -> { + lowerBound + } + else -> { + upperBound + } + } + } + + /** + * 高斯模糊 + */ + fun stackBlur(srcBitmap: Bitmap?): Bitmap? { + if (srcBitmap == null) return null + val rs = RenderScript.create(App.INSTANCE) + val blurredBitmap = srcBitmap.copy(Config.ARGB_8888, true) + + //分配用于渲染脚本的内存 + val input = Allocation.createFromBitmap( + rs, + blurredBitmap, + Allocation.MipmapControl.MIPMAP_FULL, + Allocation.USAGE_SHARED + ) + val output = Allocation.createTyped(rs, input.type) + + //加载我们想要使用的特定脚本的实例。 + val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)) + script.setInput(input) + + //设置模糊半径 + script.setRadius(8f) + + //启动 ScriptIntrinsicBlur + script.forEach(output) + + //将输出复制到模糊的位图 + output.copyTo(blurredBitmap) + + return blurredBitmap + } + + fun getMeanColor(bitmap: Bitmap): Int { + val width: Int = bitmap.width + val height: Int = bitmap.height + var pixel: Int + var pixelSumRed = 0 + var pixelSumBlue = 0 + var pixelSumGreen = 0 + for (i in 0..99) { + for (j in 70..99) { + pixel = bitmap.getPixel( + (i * width / 100.toFloat()).roundToInt(), + (j * height / 100.toFloat()).roundToInt() + ) + pixelSumRed += Color.red(pixel) + pixelSumGreen += Color.green(pixel) + pixelSumBlue += Color.blue(pixel) + } + } + val averagePixelRed = pixelSumRed / 3000 + val averagePixelBlue = pixelSumBlue / 3000 + val averagePixelGreen = pixelSumGreen / 3000 + return Color.rgb( + averagePixelRed + 3, + averagePixelGreen + 3, + averagePixelBlue + 3 + ) + } + +} diff --git a/app/src/main/java/com/novel/read/utils/BooksDiffCallBack.kt b/app/src/main/java/com/novel/read/utils/BooksDiffCallBack.kt new file mode 100644 index 0000000..f73fd5a --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/BooksDiffCallBack.kt @@ -0,0 +1,76 @@ +package com.novel.read.utils + +import androidx.annotation.NonNull +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import com.novel.read.data.db.entity.Book + +class BooksDiffCallBack : DiffUtil.ItemCallback() { + /** + * 判断是否是同一个item + * + * @param oldItem New data + * @param newItem old Data + * @return + */ + override fun areItemsTheSame( + @NonNull oldItem: Book, + @NonNull newItem: Book + ): Boolean { + return oldItem.bookId === newItem.bookId + } + + /** + * 当是同一个item时,再判断内容是否发生改变 + * + * @param oldItem New data + * @param newItem old Data + * @return + */ + override fun areContentsTheSame( + @NonNull oldItem: Book, + @NonNull newItem: Book + ): Boolean { + return (oldItem.bookName.equals(newItem.bookName) + && oldItem.introduction.equals(newItem.introduction) + && oldItem.lastUpdateChapterDate.equals(newItem.lastUpdateChapterDate)) + } + + /** + * 可选实现 + * 如果需要精确修改某一个view中的内容,请实现此方法。 + * 如果不实现此方法,或者返回null,将会直接刷新整个item。 + * + * @param oldItem Old data + * @param newItem New data + * @return Payload info. if return null, the entire item will be refreshed. + */ + override fun getChangePayload( + @NonNull oldItem: Book, + @NonNull newItem: Book + ): Any? { + val bundle = bundleOf() + if (oldItem.bookName != newItem.bookName) { + bundle.putString("name", newItem.bookName) + } + if (oldItem.authorPenname != newItem.authorPenname) { + bundle.putString("author", newItem.authorPenname) + } + if (oldItem.durChapterTitle != newItem.durChapterTitle) { + bundle.putString("dur", newItem.durChapterTitle) + } +// if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { +// bundle.putString("last", newItem.latestChapterTitle) +// } + if (oldItem.coverImageUrl != newItem.coverImageUrl) { + bundle.putString("cover", newItem.coverImageUrl) + } + if (oldItem.lastUpdateChapterDate != newItem.lastUpdateChapterDate + || oldItem.durChapterTime != newItem.durChapterTime + ) { + bundle.putBoolean("refresh", true) + } + if (bundle.isEmpty) return null + return bundle + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ColorUtils.kt b/app/src/main/java/com/novel/read/utils/ColorUtils.kt new file mode 100644 index 0000000..ab1ff27 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ColorUtils.kt @@ -0,0 +1,252 @@ +package com.novel.read.utils + +import android.graphics.Color + +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import java.util.* +import kotlin.math.* + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ColorUtils { + + fun intToString(intColor: Int): String { + return String.format("#%06X", 0xFFFFFF and intColor) + } + + + fun stripAlpha(@ColorInt color: Int): Int { + return -0x1000000 or color + } + + @ColorInt + fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int { + if (by == 1f) return color + val alpha = Color.alpha(color) + val hsv = FloatArray(3) + Color.colorToHSV(color, hsv) + hsv[2] *= by // value component + return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv)) + } + + @ColorInt + fun darkenColor(@ColorInt color: Int): Int { + return shiftColor(color, 0.9f) + } + + @ColorInt + fun lightenColor(@ColorInt color: Int): Int { + return shiftColor(color, 1.1f) + } + + fun isColorLight(@ColorInt color: Int): Boolean { + val darkness = + 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255 + return darkness < 0.4 + } + + @ColorInt + fun invertColor(@ColorInt color: Int): Int { + val r = 255 - Color.red(color) + val g = 255 - Color.green(color) + val b = 255 - Color.blue(color) + return Color.argb(Color.alpha(color), r, g, b) + } + + @ColorInt + fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int { + val alpha = (Color.alpha(color) * factor).roundToInt() + val red = Color.red(color) + val green = Color.green(color) + val blue = Color.blue(color) + return Color.argb(alpha, red, green, blue) + } + + @ColorInt + fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int { + val a = min(255, max(0, (alpha * 255).toInt())) shl 24 + val rgb = 0x00ffffff and baseColor + return a + rgb + } + + /** + * Taken from CollapsingToolbarLayout's CollapsingTextHelper class. + */ + fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int { + val inverseRatio = 1f - ratio + val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio + val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio + val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio + val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio + return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt()) + } + + /** + * 按条件的到随机颜色 + * + * @param alpha 透明 + * @param lower 下边界 + * @param upper 上边界 + * @return 颜色值 + */ + fun getRandomColor(alpha: Int, lower: Int, upper: Int): Int { + return RandomColor(alpha, lower, upper).color + } + + /** + * @return 获取随机色 + */ + fun getRandomColor(): Int { + return RandomColor(255, 80, 200).color + } + + + /** + * 随机颜色 + */ + class RandomColor(alpha: Int, lower: Int, upper: Int) { + private var alpha: Int = 0 + private var lower: Int = 0 + private var upper: Int = 0 + + //随机数是前闭 后开 + val color: Int + get() { + val red = getLower() + Random().nextInt(getUpper() - getLower() + 1) + val green = getLower() + Random().nextInt(getUpper() - getLower() + 1) + val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1) + + return Color.argb(getAlpha(), red, green, blue) + } + + init { + require(upper > lower) { "must be lower < upper" } + setAlpha(alpha) + setLower(lower) + setUpper(upper) + } + + private fun getAlpha(): Int { + return alpha + } + + private fun setAlpha(alpha: Int) { + var alpha1 = alpha + if (alpha1 > 255) alpha1 = 255 + if (alpha1 < 0) alpha1 = 0 + this.alpha = alpha1 + } + + private fun getLower(): Int { + return lower + } + + private fun setLower(lower: Int) { + var lower1 = lower + if (lower1 < 0) lower1 = 0 + this.lower = lower1 + } + + private fun getUpper(): Int { + return upper + } + + private fun setUpper(upper: Int) { + var upper1 = upper + if (upper1 > 255) upper1 = 255 + this.upper = upper1 + } + } + + fun argb(R: Int, G: Int, B: Int): Int { + return argb(Byte.MAX_VALUE.toInt(), R, G, B) + } + + fun argb(A: Int, R: Int, G: Int, B: Int): Int { + val colorByteArr = + byteArrayOf(A.toByte(), R.toByte(), G.toByte(), B.toByte()) + return byteArrToInt(colorByteArr) + } + + fun rgb(argb: Int): IntArray? { + return intArrayOf(argb shr 16 and 0xFF, argb shr 8 and 0xFF, argb and 0xFF) + } + + fun byteArrToInt(colorByteArr: ByteArray): Int { + return ((colorByteArr[0].toInt() shl 24) + (colorByteArr[1].toInt() and 0xFF shl 16) + + (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF)) + } + + fun rgb2lab(R: Int, G: Int, B: Int): IntArray { + val x: Float + val y: Float + val z: Float + val fx: Float + val fy: Float + val fz: Float + val xr: Float + val yr: Float + val zr: Float + val ls: Float + val `as`: Float + val bs: Float + val eps = 216f / 24389f + val k = 24389f / 27f + val xr1 = 0.964221f // reference white D50 + val yr1 = 1.0f + val zr1 = 0.825211f + + // RGB to XYZ + var r: Float = R / 255f //R 0..1 + var g: Float = G / 255f //G 0..1 + var b: Float = B / 255f //B 0..1 + + // assuming sRGB (D65) + r = if (r <= 0.04045) r / 12 else ((r + 0.055) / 1.055).pow(2.4).toFloat() + g = if (g <= 0.04045) g / 12 else ((g + 0.055) / 1.055).pow(2.4).toFloat() + b = if (b <= 0.04045) b / 12 else ((b + 0.055) / 1.055).pow(2.4).toFloat() + x = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b + y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b + z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b + + // XYZ to Lab + xr = x / xr1 + yr = y / yr1 + zr = z / zr1 + fx = if (xr > eps) xr.toDouble().pow(1 / 3.0) + .toFloat() else ((k * xr + 16.0) / 116.0).toFloat() + fy = if (yr > eps) yr.toDouble().pow(1 / 3.0) + .toFloat() else ((k * yr + 16.0) / 116.0).toFloat() + fz = if (zr > eps) zr.toDouble().pow(1 / 3.0) + .toFloat() else ((k * zr + 16.0) / 116).toFloat() + ls = 116 * fy - 16 + `as` = 500 * (fx - fy) + bs = 200 * (fy - fz) + val lab = IntArray(3) + lab[0] = (2.55 * ls + .5).toInt() + lab[1] = (`as` + .5).toInt() + lab[2] = (bs + .5).toInt() + return lab + } + + /** + * Computes the difference between two RGB colors by converting them to the L*a*b scale and + * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76} + */ + fun getColorDifference(a: Int, b: Int): Double { + val r1: Int = Color.red(a) + val g1: Int = Color.green(a) + val b1: Int = Color.blue(a) + val r2: Int = Color.red(b) + val g2: Int = Color.green(b) + val b2: Int = Color.blue(b) + val lab1 = rgb2lab(r1, g1, b1) + val lab2 = rgb2lab(r2, g2, b2) + return sqrt( + (lab2[0] - lab1[0].toDouble()) + .pow(2.0) + (lab2[1] - lab1[1].toDouble()) + .pow(2.0) + (lab2[2] - lab1[2].toDouble()) + .pow(2.0) + ) + } +} diff --git a/app/src/main/java/com/novel/read/utils/ConvertUtils.kt b/app/src/main/java/com/novel/read/utils/ConvertUtils.kt new file mode 100644 index 0000000..f85eb39 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ConvertUtils.kt @@ -0,0 +1,122 @@ +package com.novel.read.utils + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.text.DecimalFormat + +/** + * 数据类型转换、单位转换 + * + * @author 李玉江[QQ:1023694760] + * @since 2014-4-18 + */ +@Suppress("MemberVisibilityCanBePrivate") +object ConvertUtils { + const val GB: Long = 1073741824 + const val MB: Long = 1048576 + const val KB: Long = 1024 + + fun toInt(obj: Any): Int { + return try { + Integer.parseInt(obj.toString()) + } catch (e: NumberFormatException) { + -1 + } + } + + fun toInt(bytes: ByteArray): Int { + var result = 0 + var byte: Byte + for (i in bytes.indices) { + byte = bytes[i] + result += (byte.toInt() and 0xFF).shl(8 * i) + } + return result + } + + fun toFloat(obj: Any): Float { + return try { + java.lang.Float.parseFloat(obj.toString()) + } catch (e: NumberFormatException) { + -1f + } + } + + fun toString(objects: Array, tag: String): String { + val sb = StringBuilder() + for (`object` in objects) { + sb.append(`object`) + sb.append(tag) + } + return sb.toString() + } + + @JvmOverloads + fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? { + var bitmap: Bitmap? = null + if (bytes.isNotEmpty()) { + try { + val options = BitmapFactory.Options() + // 设置让解码器以最佳方式解码 + options.inPreferredConfig = null + if (width > 0 && height > 0) { + options.outWidth = width + options.outHeight = height + } + bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) + bitmap!!.density = 96// 96 dpi + } catch (e: Exception) { + } + } + return bitmap + } + + private fun toDrawable(bitmap: Bitmap?): Drawable? { + return if (bitmap == null) null else BitmapDrawable(Resources.getSystem(), bitmap) + } + + fun toDrawable(bytes: ByteArray): Drawable? { + return toDrawable(toBitmap(bytes)) + } + + fun toFileSizeString(fileSize: Long): String { + val df = DecimalFormat("0.00") + val fileSizeString: String + fileSizeString = when { + fileSize < KB -> fileSize.toString() + "B" + fileSize < MB -> df.format(fileSize.toDouble() / KB) + "K" + fileSize < GB -> df.format(fileSize.toDouble() / MB) + "M" + else -> df.format(fileSize.toDouble() / GB) + "G" + } + return fileSizeString + } + + @JvmOverloads + fun toString(`is`: InputStream, charset: String = "utf-8"): String { + val sb = StringBuilder() + try { + val reader = BufferedReader(InputStreamReader(`is`, charset)) + while (true) { + val line = reader.readLine() + if (line == null) { + break + } else { + sb.append(line).append("\n") + } + } + reader.close() + `is`.close() + } catch (e: IOException) { + } + + return sb.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/DrawableUtils.kt b/app/src/main/java/com/novel/read/utils/DrawableUtils.kt new file mode 100644 index 0000000..daf0806 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/DrawableUtils.kt @@ -0,0 +1,48 @@ +package com.novel.read.utils + +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.TransitionDrawable +import androidx.annotation.ColorInt +import androidx.core.graphics.drawable.DrawableCompat + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused") +object DrawableUtils { + + fun createTransitionDrawable(@ColorInt startColor: Int, @ColorInt endColor: Int): TransitionDrawable { + return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor)) + } + + fun createTransitionDrawable(start: Drawable, end: Drawable): TransitionDrawable { + val drawables = arrayOfNulls(2) + + drawables[0] = start + drawables[1] = end + + return TransitionDrawable(drawables) + } + + fun setTintList(drawable: Drawable?, tint: ColorStateList, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP) { + drawable?.let { + val wrappedDrawable = DrawableCompat.wrap(it) + wrappedDrawable.mutate() + DrawableCompat.setTintMode(wrappedDrawable, tintMode) + DrawableCompat.setTintList(wrappedDrawable, tint) + } + } + + + fun setTint(drawable: Drawable?, @ColorInt tint: Int, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP) { + drawable?.let { + val wrappedDrawable = DrawableCompat.wrap(it) + wrappedDrawable.mutate() + DrawableCompat.setTintMode(wrappedDrawable, tintMode) + DrawableCompat.setTint(wrappedDrawable, tint) + } + } +} diff --git a/app/src/main/java/com/novel/read/utils/FileUtils.kt b/app/src/main/java/com/novel/read/utils/FileUtils.kt new file mode 100644 index 0000000..57aec96 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/FileUtils.kt @@ -0,0 +1,774 @@ +package com.novel.read.utils + +import android.os.Environment +import android.webkit.MimeTypeMap +import androidx.annotation.IntDef +import com.novel.read.App +import java.io.* +import java.nio.charset.Charset +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern + +@Suppress("unused") +object FileUtils { + + fun exists(root: File, vararg subDirFiles: String): Boolean { + return getFile(root, *subDirFiles).exists() + } + + fun createFileIfNotExist(root: File, vararg subDirFiles: String): File { + val filePath = getPath(root, *subDirFiles) + return createFileIfNotExist(filePath) + } + + fun createFolderIfNotExist(root: File, vararg subDirs: String): File { + val filePath = getPath(root, *subDirs) + return createFolderIfNotExist(filePath) + } + + fun createFolderIfNotExist(filePath: String): File { + val file = File(filePath) + //如果文件夹不存在,就创建它 + if (!file.exists()) { + file.mkdirs() + } + return file + } + + @Synchronized + fun createFileIfNotExist(filePath: String): File { + val file = File(filePath) + try { + if (!file.exists()) { + //创建父类文件夹 + file.parent?.let { + createFolderIfNotExist(it) + } + //创建文件 + file.createNewFile() + } + } catch (e: IOException) { + e.printStackTrace() + } + return file + } + + fun createFileWithReplace(filePath: String) : File{ + val file = File(filePath) + if (!file.exists()) { + //创建父类文件夹 + file.parent?.let { + createFolderIfNotExist(it) + } + //创建文件 + file.createNewFile() + } + else{ + file.delete() + file.createNewFile() + } + return file + } + + fun getFile(root: File, vararg subDirFiles: String): File { + val filePath = getPath(root, *subDirFiles) + return File(filePath) + } + + fun getPath(root: File, vararg subDirFiles: String): String { + val path = StringBuilder(root.absolutePath) + subDirFiles.forEach { + if (it.isNotEmpty()) { + path.append(File.separator).append(it) + } + } + return path.toString() + } + + //递归删除文件夹下的数据 + @Synchronized + fun deleteFile(filePath: String) { + val file = File(filePath) + if (!file.exists()) return + + if (file.isDirectory) { + val files = file.listFiles() + files?.forEach { subFile -> + val path = subFile.path + deleteFile(path) + } + } + //删除文件 + file.delete() + } + + fun getCachePath(): String { + return App.INSTANCE.externalCacheDir?.absolutePath + ?: App.INSTANCE.cacheDir.absolutePath + } + + fun getSdCardPath(): String { + @Suppress("DEPRECATION") + var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath + try { + sdCardDirectory = File(sdCardDirectory).canonicalPath + } catch (ioe: IOException) { + ioe.printStackTrace() + } + return sdCardDirectory + } + + const val BY_NAME_ASC = 0 + const val BY_NAME_DESC = 1 + const val BY_TIME_ASC = 2 + const val BY_TIME_DESC = 3 + const val BY_SIZE_ASC = 4 + const val BY_SIZE_DESC = 5 + const val BY_EXTENSION_ASC = 6 + const val BY_EXTENSION_DESC = 7 + + @IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC]) + @kotlin.annotation.Retention(AnnotationRetention.SOURCE) + annotation class SortType + + /** + * 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符 + */ + fun separator(path: String): String { + var path1 = path + val separator = File.separator + path1 = path1.replace("\\", separator) + if (!path1.endsWith(separator)) { + path1 += separator + } + return path1 + } + + fun closeSilently(c: Closeable?) { + if (c == null) { + return + } + try { + c.close() + } catch (ignored: IOException) { + } + + } + + /** + * 列出指定目录下的所有子目录 + */ + @JvmOverloads + fun listDirs( + startDirPath: String, + excludeDirs: Array? = null, @SortType sortType: Int = BY_NAME_ASC + ): Array { + var excludeDirs1 = excludeDirs + val dirList = ArrayList() + val startDir = File(startDirPath) + if (!startDir.isDirectory) { + return arrayOfNulls(0) + } + val dirs = startDir.listFiles(FileFilter { f -> + if (f == null) { + return@FileFilter false + } + f.isDirectory + }) + ?: return arrayOfNulls(0) + if (excludeDirs1 == null) { + excludeDirs1 = arrayOf() + } + for (dir in dirs) { + val file = dir.absoluteFile + if (!excludeDirs1.contentDeepToString().contains(file.name)) { + dirList.add(file) + } + } + when (sortType) { + BY_NAME_ASC -> Collections.sort(dirList, SortByName()) + BY_NAME_DESC -> { + Collections.sort(dirList, SortByName()) + dirList.reverse() + } + BY_TIME_ASC -> Collections.sort(dirList, SortByTime()) + BY_TIME_DESC -> { + Collections.sort(dirList, SortByTime()) + dirList.reverse() + } + BY_SIZE_ASC -> Collections.sort(dirList, SortBySize()) + BY_SIZE_DESC -> { + Collections.sort(dirList, SortBySize()) + dirList.reverse() + } + BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension()) + BY_EXTENSION_DESC -> { + Collections.sort(dirList, SortByExtension()) + dirList.reverse() + } + } + return dirList.toTypedArray() + } + + /** + * 列出指定目录下的所有子目录及所有文件 + */ + @JvmOverloads + fun listDirsAndFiles( + startDirPath: String, + allowExtensions: Array? = null + ): Array? { + val dirs: Array? + val files: Array? = if (allowExtensions == null) { + listFiles(startDirPath) + } else { + listFiles(startDirPath, allowExtensions) + } + val dirsAndFiles: Array + dirs = listDirs(startDirPath) + if (files == null) { + return null + } + dirsAndFiles = arrayOfNulls(dirs.size + files.size) + System.arraycopy(dirs, 0, dirsAndFiles, 0, dirs.size) + System.arraycopy(files, 0, dirsAndFiles, dirs.size, files.size) + return dirsAndFiles + } + + /** + * 列出指定目录下的所有文件 + */ + @JvmOverloads + fun listFiles( + startDirPath: String, + filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC + ): Array { + val fileList = ArrayList() + val f = File(startDirPath) + if (!f.isDirectory) { + return arrayOfNulls(0) + } + val files = f.listFiles(FileFilter { file -> + if (file == null) { + return@FileFilter false + } + if (file.isDirectory) { + return@FileFilter false + } + + filterPattern?.matcher(file.name)?.find() ?: true + }) + ?: return arrayOfNulls(0) + for (file in files) { + fileList.add(file.absoluteFile) + } + when (sortType) { + BY_NAME_ASC -> Collections.sort(fileList, SortByName()) + BY_NAME_DESC -> { + Collections.sort(fileList, SortByName()) + fileList.reverse() + } + BY_TIME_ASC -> Collections.sort(fileList, SortByTime()) + BY_TIME_DESC -> { + Collections.sort(fileList, SortByTime()) + fileList.reverse() + } + BY_SIZE_ASC -> Collections.sort(fileList, SortBySize()) + BY_SIZE_DESC -> { + Collections.sort(fileList, SortBySize()) + fileList.reverse() + } + BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension()) + BY_EXTENSION_DESC -> { + Collections.sort(fileList, SortByExtension()) + fileList.reverse() + } + } + return fileList.toTypedArray() + } + + /** + * 列出指定目录下的所有文件 + */ + fun listFiles(startDirPath: String, allowExtensions: Array?): Array? { + val file = File(startDirPath) + return file.listFiles { _, name -> + //返回当前目录所有以某些扩展名结尾的文件 + val extension = getExtension(name) + allowExtensions?.contentDeepToString()?.contains(extension) == true + || allowExtensions == null + } + } + + /** + * 列出指定目录下的所有文件 + */ + fun listFiles(startDirPath: String, allowExtension: String?): Array? { + return if (allowExtension == null) + listFiles(startDirPath, allowExtension = null) + else + listFiles(startDirPath, arrayOf(allowExtension)) + } + + /** + * 判断文件或目录是否存在 + */ + fun exist(path: String): Boolean { + val file = File(path) + return file.exists() + } + + /** + * 删除文件或目录 + */ + @JvmOverloads + fun delete(file: File, deleteRootDir: Boolean = false): Boolean { + var result = false + if (file.isFile) { + //是文件 + result = deleteResolveEBUSY(file) + } else { + //是目录 + val files = file.listFiles() ?: return false + if (files.isEmpty()) { + result = deleteRootDir && deleteResolveEBUSY(file) + } else { + for (f in files) { + delete(f, deleteRootDir) + result = deleteResolveEBUSY(f) + } + } + if (deleteRootDir) { + result = deleteResolveEBUSY(file) + } + } + return result + } + + /** + * bug: open failed: EBUSY (Device or resource busy) + * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy + */ + private fun deleteResolveEBUSY(file: File): Boolean { + // Before you delete a Directory or File: rename it! + val to = File(file.absolutePath + System.currentTimeMillis()) + + file.renameTo(to) + return to.delete() + } + + /** + * 删除文件或目录 + */ + @JvmOverloads + fun delete(path: String, deleteRootDir: Boolean = false): Boolean { + val file = File(path) + + return if (file.exists()) { + delete(file, deleteRootDir) + } else false + } + + /** + * 复制文件为另一个文件,或复制某目录下的所有文件及目录到另一个目录下 + */ + fun copy(src: String, tar: String): Boolean { + val srcFile = File(src) + return srcFile.exists() && copy(srcFile, File(tar)) + } + + /** + * 复制文件或目录 + */ + fun copy(src: File, tar: File): Boolean { + try { + if (src.isFile) { + val `is` = FileInputStream(src) + val op = FileOutputStream(tar) + val bis = BufferedInputStream(`is`) + val bos = BufferedOutputStream(op) + val bt = ByteArray(1024 * 8) + while (true) { + val len = bis.read(bt) + if (len == -1) { + break + } else { + bos.write(bt, 0, len) + } + } + bis.close() + bos.close() + } else if (src.isDirectory) { + tar.mkdirs() + src.listFiles()?.forEach { file -> + copy(file.absoluteFile, File(tar.absoluteFile, file.name)) + } + } + return true + } catch (e: Exception) { + return false + } + + } + + /** + * 移动文件或目录 + */ + fun move(src: String, tar: String): Boolean { + return move(File(src), File(tar)) + } + + /** + * 移动文件或目录 + */ + fun move(src: File, tar: File): Boolean { + return rename(src, tar) + } + + /** + * 文件重命名 + */ + fun rename(oldPath: String, newPath: String): Boolean { + return rename(File(oldPath), File(newPath)) + } + + /** + * 文件重命名 + */ + fun rename(src: File, tar: File): Boolean { + return src.renameTo(tar) + } + + /** + * 读取文本文件, 失败将返回空串 + */ + @JvmOverloads + fun readText(filepath: String, charset: String = "utf-8"): String { + try { + val data = readBytes(filepath) + if (data != null) { + return String(data, Charset.forName(charset)).trim { it <= ' ' } + } + } catch (ignored: UnsupportedEncodingException) { + } + + return "" + } + + /** + * 读取文件内容, 失败将返回空串 + */ + fun readBytes(filepath: String): ByteArray? { + var fis: FileInputStream? = null + try { + fis = FileInputStream(filepath) + val baos = ByteArrayOutputStream() + val buffer = ByteArray(1024) + while (true) { + val len = fis.read(buffer, 0, buffer.size) + if (len == -1) { + break + } else { + baos.write(buffer, 0, len) + } + } + val data = baos.toByteArray() + baos.close() + return data + } catch (e: IOException) { + return null + } finally { + closeSilently(fis) + } + } + + /** + * 保存文本内容 + */ + @JvmOverloads + fun writeText(filepath: String, content: String, charset: String = "utf-8"): Boolean { + try { + writeBytes(filepath, content.toByteArray(charset(charset))) + return true + } catch (e: UnsupportedEncodingException) { + return false + } + + } + + /** + * 保存文件内容 + */ + fun writeBytes(filepath: String, data: ByteArray): Boolean { + val file = File(filepath) + var fos: FileOutputStream? = null + return try { + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } + fos = FileOutputStream(filepath) + fos.write(data) + true + } catch (e: IOException) { + false + } finally { + closeSilently(fos) + } + } + + /** + * 追加文本内容 + */ + fun appendText(path: String, content: String): Boolean { + val file = File(path) + var writer: FileWriter? = null + return try { + if (!file.exists()) { + + file.createNewFile() + } + writer = FileWriter(file, true) + writer.write(content) + true + } catch (e: IOException) { + false + } finally { + closeSilently(writer) + } + } + + /** + * 获取文件大小 + */ + fun getLength(path: String): Long { + val file = File(path) + return if (!file.isFile || !file.exists()) { + 0 + } else file.length() + } + + /** + * 获取文件或网址的名称(包括后缀) + */ + fun getName(pathOrUrl: String?): String { + if (pathOrUrl == null) { + return "" + } + val pos = pathOrUrl.lastIndexOf('/') + return if (0 <= pos) { + pathOrUrl.substring(pos + 1) + } else { + System.currentTimeMillis().toString() + "." + getExtension(pathOrUrl) + } + } + + /** + * 获取文件名(不包括扩展名) + */ + fun getNameExcludeExtension(path: String): String { + try { + var fileName = File(path).name + val lastIndexOf = fileName.lastIndexOf(".") + if (lastIndexOf != -1) { + fileName = fileName.substring(0, lastIndexOf) + } + return fileName + } catch (e: Exception) { + return "" + } + + } + + /** + * 获取格式化后的文件大小 + */ + fun getSize(path: String): String { + val fileSize = getLength(path) + return ConvertUtils.toFileSizeString(fileSize) + } + + /** + * 获取文件后缀,不包括“.” + */ + fun getExtension(pathOrUrl: String): String { + val dotPos = pathOrUrl.lastIndexOf('.') + return if (0 <= dotPos) { + pathOrUrl.substring(dotPos + 1) + } else { + "ext" + } + } + + /** + * 获取文件的MIME类型 + */ + fun getMimeType(pathOrUrl: String): String { + val ext = getExtension(pathOrUrl) + val map = MimeTypeMap.getSingleton() + return map.getMimeTypeFromExtension(ext) ?: "*/*" + } + + /** + * 获取格式化后的文件/目录创建或最后修改时间 + */ + @JvmOverloads + fun getDateTime(path: String, format: String = "yyyy年MM月dd日HH:mm"): String { + val file = File(path) + return getDateTime(file, format) + } + + /** + * 获取格式化后的文件/目录创建或最后修改时间 + */ + fun getDateTime(file: File, format: String): String { + val cal = Calendar.getInstance() + cal.timeInMillis = file.lastModified() + return SimpleDateFormat(format, Locale.PRC).format(cal.time) + } + + /** + * 比较两个文件的最后修改时间 + */ + fun compareLastModified(path1: String, path2: String): Int { + val stamp1 = File(path1).lastModified() + val stamp2 = File(path2).lastModified() + return when { + stamp1 > stamp2 -> { + 1 + } + stamp1 < stamp2 -> { + -1 + } + else -> { + 0 + } + } + } + + /** + * 创建多级别的目录 + */ + fun makeDirs(path: String): Boolean { + return makeDirs(File(path)) + } + + /** + * 创建多级别的目录 + */ + fun makeDirs(file: File): Boolean { + return file.mkdirs() + } + + class SortByExtension : Comparator { + + override fun compare(f1: File?, f2: File?): Int { + return if (f1 == null || f2 == null) { + if (f1 == null) { + -1 + } else { + 1 + } + } else { + if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + f1.name.compareTo(f2.name, ignoreCase = true) + } + } + } + + } + + class SortByName : Comparator { + private var caseSensitive: Boolean = false + + constructor(caseSensitive: Boolean) { + this.caseSensitive = caseSensitive + } + + constructor() { + this.caseSensitive = false + } + + override fun compare(f1: File?, f2: File?): Int { + if (f1 == null || f2 == null) { + return if (f1 == null) { + -1 + } else { + 1 + } + } else { + return if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + val s1 = f1.name + val s2 = f2.name + if (caseSensitive) { + s1.compareTo(s2) + } else { + s1.compareTo(s2, ignoreCase = true) + } + } + } + } + + } + + class SortBySize : Comparator { + + override fun compare(f1: File?, f2: File?): Int { + return if (f1 == null || f2 == null) { + if (f1 == null) { + -1 + } else { + 1 + } + } else { + if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + if (f1.length() < f2.length()) { + -1 + } else { + 1 + } + } + } + } + + } + + class SortByTime : Comparator { + + override fun compare(f1: File?, f2: File?): Int { + return if (f1 == null || f2 == null) { + if (f1 == null) { + -1 + } else { + 1 + } + } else { + if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + if (f1.lastModified() > f2.lastModified()) { + -1 + } else { + 1 + } + } + } + } + + } +} diff --git a/app/src/main/java/com/novel/read/utils/LanguageUtils.kt b/app/src/main/java/com/novel/read/utils/LanguageUtils.kt new file mode 100644 index 0000000..dc27355 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/LanguageUtils.kt @@ -0,0 +1,105 @@ +package com.novel.read.utils + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Build +import android.os.LocaleList +import com.novel.read.constant.PreferKey +import com.novel.read.utils.ext.getPrefString +import java.util.* + + +object LanguageUtils { + + /** + * 设置语言 + */ + fun setConfiguration(context: Context): Context { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val resources: Resources = context.resources + val metrics = resources.displayMetrics + val configuration: Configuration = resources.configuration + //Log.d("h11128", "set language to ${context.getPrefString(PreferKey.language)}") + val targetLocale = getSetLocale(context) + configuration.setLocale(targetLocale) + configuration.setLocales(LocaleList(targetLocale)) + @Suppress("DEPRECATION") + resources.updateConfiguration(configuration, metrics) + context.createConfigurationContext(configuration) + } else { + setConfigurationOld(context) + context + } + } + + /** + * 设置语言 + */ + private fun setConfigurationOld(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + val resources: Resources = context.resources + val targetLocale = getSetLocale(context) + val configuration: Configuration = resources.configuration + @Suppress("DEPRECATION") + configuration.locale = targetLocale + @Suppress("DEPRECATION") + resources.updateConfiguration(configuration, resources.displayMetrics) + } + } + + /** + * 当前系统语言 + */ + private fun getSystemLocale(): Locale { + val locale: Locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言 + locale = Resources.getSystem().configuration.locales.get(0) + } else { + @Suppress("DEPRECATION") + locale = Resources.getSystem().configuration.locale + } + return locale + } + + /** + * 当前App语言 + */ + private fun getAppLocale(context: Context): Locale { + val locale: Locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + locale = context.resources.configuration.locales[0] + } else { + @Suppress("DEPRECATION") + locale = context.resources.configuration.locale + } + return locale + + } + + /** + * 当前设置语言 + */ + private fun getSetLocale(context: Context): Locale { + return when (context.getPrefString(PreferKey.language)) { + "zh" -> Locale.SIMPLIFIED_CHINESE + "tw" -> Locale.TRADITIONAL_CHINESE + "en" -> Locale.ENGLISH + else -> getSystemLocale() + } + } + + /** + * 判断App语言和设置语言是否相同 + */ + fun isSameWithSetting(context: Context): Boolean { + val locale = getAppLocale(context) + val language = locale.language + val country = locale.country + val pfLocale = getSetLocale(context) + val pfLanguage = pfLocale.language + val pfCountry = pfLocale.country + return language == pfLanguage && country == pfCountry + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/MD5Utils.kt b/app/src/main/java/com/novel/read/utils/MD5Utils.kt new file mode 100644 index 0000000..47158b9 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/MD5Utils.kt @@ -0,0 +1,39 @@ +package com.novel.read.utils + +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * 将字符串转化为MD5 + */ +@Suppress("unused") +object MD5Utils { + + fun md5Encode(str: String?): String { + if (str == null) return "" + var reStr = "" + try { + val md5: MessageDigest = MessageDigest.getInstance("MD5") + val bytes: ByteArray = md5.digest(str.toByteArray()) + val stringBuffer: StringBuilder = StringBuilder() + for (b in bytes) { + val bt: Int = b.toInt() and 0xff + if (bt < 16) { + stringBuffer.append(0) + } + stringBuffer.append(Integer.toHexString(bt)) + } + reStr = stringBuffer.toString() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + + return reStr + } + + fun md5Encode16(str: String): String { + var reStr = md5Encode(str) + reStr = reStr.substring(8, 24) + return reStr + } +} diff --git a/app/src/main/java/com/novel/read/utils/NetworkUtils.kt b/app/src/main/java/com/novel/read/utils/NetworkUtils.kt new file mode 100644 index 0000000..d0b7ba7 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/NetworkUtils.kt @@ -0,0 +1,155 @@ +package com.novel.read.utils + +import retrofit2.Response +import java.net.InetAddress +import java.net.NetworkInterface +import java.net.SocketException +import java.net.URL +import java.util.* +import java.util.regex.Pattern + +@Suppress("unused") +object NetworkUtils { + fun getUrl(response: Response<*>): String { + val networkResponse = response.raw().networkResponse() + return networkResponse?.request()?.url()?.toString() + ?: response.raw().request().url().toString() + } + + private val notNeedEncoding: BitSet by lazy { + val bitSet = BitSet(256) + for (i in 'a'.toInt()..'z'.toInt()) { + bitSet.set(i) + } + for (i in 'A'.toInt()..'Z'.toInt()) { + bitSet.set(i) + } + for (i in '0'.toInt()..'9'.toInt()) { + bitSet.set(i) + } + for (char in "+-_.$:()!*@&#,[]") { + bitSet.set(char.toInt()) + } + return@lazy bitSet + } + + /** + * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' + * 0-9a-zA-Z保留

+ * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留 + * 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F] + */ + fun hasUrlEncoded(str: String): Boolean { + var needEncode = false + var i = 0 + while (i < str.length) { + val c = str[i] + if (notNeedEncoding.get(c.toInt())) { + i++ + continue + } + if (c == '%' && i + 2 < str.length) { + // 判断是否符合urlEncode规范 + val c1 = str[++i] + val c2 = str[++i] + if (isDigit16Char(c1) && isDigit16Char(c2)) { + i++ + continue + } + } + // 其他字符,肯定需要urlEncode + needEncode = true + break + } + + return !needEncode + } + + /** + * 判断c是否是16进制的字符 + */ + private fun isDigit16Char(c: Char): Boolean { + return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f' + } + + /** + * 获取绝对地址 + */ + fun getAbsoluteURL(baseURL: String?, relativePath: String?): String? { + if (baseURL.isNullOrEmpty()) return relativePath + if (relativePath.isNullOrEmpty()) return baseURL + var relativeUrl = relativePath + try { + val absoluteUrl = URL(baseURL.substringBefore(",")) + val parseUrl = URL(absoluteUrl, relativePath) + relativeUrl = parseUrl.toString() + return relativeUrl + } catch (e: Exception) { + e.printStackTrace() + } + return relativeUrl + } + + fun getBaseUrl(url: String?): String? { + if (url == null || !url.startsWith("http")) return null + val index = url.indexOf("/", 9) + return if (index == -1) { + url + } else url.substring(0, index) + } + + fun getSubDomain(url: String?): String { + var baseUrl = getBaseUrl(url) + if (baseUrl == null) return "" + return if (baseUrl.indexOf(".") == baseUrl.lastIndexOf(".")) { + baseUrl.substring(baseUrl.lastIndexOf("/")+1) + } else baseUrl.substring(baseUrl.indexOf(".")+1) + } + + /** + * Get local Ip address. + */ + fun getLocalIPAddress(): InetAddress? { + var enumeration: Enumeration? = null + try { + enumeration = NetworkInterface.getNetworkInterfaces() + } catch (e: SocketException) { + e.printStackTrace() + } + + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + val nif = enumeration.nextElement() + val addresses = nif.inetAddresses + if (addresses != null) { + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && isIPv4Address(address.hostAddress)) { + return address + } + } + } + } + } + return null + } + + /** + * Check if valid IPV4 address. + * + * @param input the address string to check for validity. + * @return True if the input parameter is a valid IPv4 address. + */ + fun isIPv4Address(input: String): Boolean { + return IPV4_PATTERN.matcher(input).matches() + } + + /** + * Ipv4 address check. + */ + private val IPV4_PATTERN = Pattern.compile( + "^(" + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}" + + "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/RealPathUtil.kt b/app/src/main/java/com/novel/read/utils/RealPathUtil.kt new file mode 100644 index 0000000..58ec9b9 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/RealPathUtil.kt @@ -0,0 +1,175 @@ +package com.novel.read.utils + +import android.annotation.SuppressLint +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException + +@Suppress("unused") +object RealPathUtil { + /** + * Method for return file path of Gallery image + * @return path of the selected image file from gallery + */ + private var filePathUri: Uri? = null + + @Suppress("DEPRECATION") + fun getPath(context: Context, uri: Uri): String? { + //check here to KITKAT or new version + @SuppressLint("ObsoleteSdkInt") + val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + filePathUri = uri + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } + } else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + //return getDataColumn(context, uri, null, null); + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + var contentUri: Uri? = null + when (type) { + "image" -> { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + "video" -> { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + "audio" -> { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + } + val selection = "_id=?" + val selectionArgs = arrayOf( + split[1] + ) + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } else if ("content".equals( + uri.scheme, + ignoreCase = true + ) + ) { // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private fun getDataColumn( + context: Context, uri: Uri?, selection: String?, + selectionArgs: Array? + ): String? { + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf( + column + ) + try { + cursor = + context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + return cursor.getString(index) + } + } catch (e: IllegalArgumentException) { + e.printStackTrace() + val file = File(context.cacheDir, "tmp") + val filePath = file.absolutePath + var input: FileInputStream? = null + var output: FileOutputStream? = null + try { + val pfd = + context.contentResolver.openFileDescriptor(filePathUri!!, "r") + ?: return null + val fd = pfd.fileDescriptor + input = FileInputStream(fd) + output = FileOutputStream(filePath) + var read: Int + val bytes = ByteArray(4096) + while (input.read(bytes).also { read = it } != -1) { + output.write(bytes, 0, read) + } + return File(filePath).absolutePath + } catch (ignored: IOException) { + ignored.printStackTrace() + } finally { + input?.close() + output?.close() + } + } finally { + cursor?.close() + } + return null + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/StringUtils.kt b/app/src/main/java/com/novel/read/utils/StringUtils.kt new file mode 100644 index 0000000..6ef2916 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/StringUtils.kt @@ -0,0 +1,353 @@ +package com.novel.read.utils + +import android.annotation.SuppressLint +import android.text.TextUtils.isEmpty +import com.hankcs.hanlp.HanLP +import com.novel.read.App +import com.novel.read.constant.PreferKey +import com.novel.read.help.AppConfig +import com.novel.read.utils.ext.getPrefInt +import com.novel.read.utils.ext.getPrefLong +import java.text.DecimalFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern +import kotlin.math.abs +import kotlin.math.log10 +import kotlin.math.pow + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object StringUtils { + private const val HOUR_OF_DAY = 24 + private const val DAY_OF_YESTERDAY = 2 + private const val TIME_UNIT = 60 + private val ChnMap = chnMap + + private val chnMap: HashMap + get() { + val map = HashMap() + var cnStr = "零一二三四五六七八九十" + var c = cnStr.toCharArray() + for (i in 0..10) { + map[c[i]] = i + } + cnStr = "〇壹贰叁肆伍陆柒捌玖拾" + c = cnStr.toCharArray() + for (i in 0..10) { + map[c[i]] = i + } + map['两'] = 2 + map['百'] = 100 + map['佰'] = 100 + map['千'] = 1000 + map['仟'] = 1000 + map['万'] = 10000 + map['亿'] = 100000000 + return map + } + + //将时间转换成日期 + fun dateConvert(time: Long, pattern: String): String { + val date = Date(time) + + @SuppressLint("SimpleDateFormat") + val format = SimpleDateFormat(pattern) + return format.format(date) + } + + //将日期转换成昨天、今天、明天 + fun dateConvert(source: String, pattern: String): String { + @SuppressLint("SimpleDateFormat") + val format = SimpleDateFormat(pattern) + val calendar = Calendar.getInstance() + try { + val date = format.parse(source) ?: return "" + val curTime = calendar.timeInMillis + calendar.time = date + //将MISC 转换成 sec + val difSec = abs((curTime - date.time) / 1000) + val difMin = difSec / 60 + val difHour = difMin / 60 + val difDate = difHour / 60 + val oldHour = calendar.get(Calendar.HOUR) + //如果没有时间 + if (oldHour == 0) { + //比日期:昨天今天和明天 + return when { + difDate == 0L -> { + "今天" + } + difDate < DAY_OF_YESTERDAY -> { + "昨天" + } + else -> { + @SuppressLint("SimpleDateFormat") + val convertFormat = SimpleDateFormat("yyyy-MM-dd") + convertFormat.format(date) + } + } + } + + return when { + difSec < TIME_UNIT -> difSec.toString() + "秒前" + difMin < TIME_UNIT -> difMin.toString() + "分钟前" + difHour < HOUR_OF_DAY -> difHour.toString() + "小时前" + difDate < DAY_OF_YESTERDAY -> "昨天" + else -> { + @SuppressLint("SimpleDateFormat") + val convertFormat = SimpleDateFormat("yyyy-MM-dd") + convertFormat.format(date) + } + } + } catch (e: ParseException) { + e.printStackTrace() + } + + return "" + } + + fun checkInstallTime(): Boolean { + val installTime = App.INSTANCE.getPrefLong(PreferKey.installTime) + val currentTime = System.currentTimeMillis() + //如果当前时间比安装时间早7天 并且 安装包开启次数大于7 + return currentTime - installTime > 7 * 24 * 3600 * 1000 && App.INSTANCE.getPrefInt( + PreferKey.installCount, + 0 + ) > 7 + + } + + fun toSize(length: Long): String { + if (length <= 0) return "0" + val units = arrayOf("b", "kb", "M", "G", "T") + //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 + //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 + val digitGroups = + (log10(length.toDouble()) / log10(1024.0)).toInt() + //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 + //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 + return DecimalFormat("#,##0.##") + .format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] + } + + @SuppressLint("DefaultLocale") + fun toFirstCapital(str: String): String { + return str.substring(0, 1).toUpperCase() + str.substring(1) + } + + /** + * 将文本中的半角字符,转换成全角字符 + */ + fun halfToFull(input: String): String { + val c = input.toCharArray() + for (i in c.indices) { + if (c[i].toInt() == 32) + //半角空格 + { + c[i] = 12288.toChar() + continue + } + //根据实际情况,过滤不需要转换的符号 + //if (c[i] == 46) //半角点号,不转换 + // continue; + + if (c[i].toInt() in 33..126) + //其他符号都转换为全角 + c[i] = (c[i].toInt() + 65248).toChar() + } + return String(c) + } + + //功能:字符串全角转换为半角 + fun fullToHalf(input: String): String { + val c = input.toCharArray() + for (i in c.indices) { + if (c[i].toInt() == 12288) + //全角空格 + { + c[i] = 32.toChar() + continue + } + + if (c[i].toInt() in 65281..65374) + c[i] = (c[i].toInt() - 65248).toChar() + } + return String(c) + } + + fun chineseNumToInt(chNum: String): Int { + var result = 0 + var tmp = 0 + var billion = 0 + val cn = chNum.toCharArray() + + // "一零二五" 形式 + if (cn.size > 1 && chNum.matches("^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$".toRegex())) { + for (i in cn.indices) { + cn[i] = (48 + ChnMap[cn[i]]!!).toChar() + } + return Integer.parseInt(String(cn)) + } + + // "一千零二十五", "一千二" 形式 + try { + for (i in cn.indices) { + val tmpNum = ChnMap[cn[i]]!! + when { + tmpNum == 100000000 -> { + result += tmp + result *= tmpNum + billion = billion * 100000000 + result + result = 0 + tmp = 0 + } + tmpNum == 10000 -> { + result += tmp + result *= tmpNum + tmp = 0 + } + tmpNum >= 10 -> { + if (tmp == 0) + tmp = 1 + result += tmpNum * tmp + tmp = 0 + } + else -> { + tmp = if (i >= 2 && i == cn.size - 1 && ChnMap[cn[i - 1]]!! > 10) + tmpNum * ChnMap[cn[i - 1]]!! / 10 + else + tmp * 10 + tmpNum + } + } + } + result += tmp + billion + return result + } catch (e: Exception) { + return -1 + } + + } + + fun stringToInt(str: String?): Int { + if (str != null) { + val num = fullToHalf(str).replace("\\s+".toRegex(), "") + return try { + Integer.parseInt(num) + } catch (e: Exception) { + chineseNumToInt(num) + } + + } + return -1 + } + + fun isContainNumber(company: String): Boolean { + val p = Pattern.compile("[0-9]+") + val m = p.matcher(company) + return m.find() + } + + fun isNumeric(str: String): Boolean { + val pattern = Pattern.compile("[0-9]+") + val isNum = pattern.matcher(str) + return isNum.matches() + } + + fun wordCountFormat(wc: String?): String { + if (wc == null) return "" + var wordsS = "" + if (isNumeric(wc)) { + val words: Int = wc.toInt() + if (words > 0) { + wordsS = words.toString() + "字" + if (words > 10000) { + val df = DecimalFormat("#.#") + wordsS = df.format(words * 1.0f / 10000f.toDouble()) + "万字" + } + } + } else { + wordsS = wc + } + return wordsS + } + + // 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格) + fun trim(s: String): String { + if (isEmpty(s)) return "" + var start = 0 + val len = s.length + var end = len - 1 + while (start < end && (s[start].toInt() <= 0x20 || s[start] == ' ')) { + ++start + } + while (start < end && (s[end].toInt() <= 0x20 || s[end] == ' ')) { + --end + } + if (end < len) ++end + return if (start > 0 || end < len) s.substring(start, end) else s + } + + fun repeat(str: String, n: Int): String { + val stringBuilder = StringBuilder() + for (i in 0 until n) { + stringBuilder.append(str) + } + return stringBuilder.toString() + } + + fun removeUTFCharacters(data: String?): String? { + if (data == null) return null + val p = Pattern.compile("\\\\u(\\p{XDigit}{4})") + val m = p.matcher(data) + val buf = StringBuffer(data.length) + while (m.find()) { + val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString() + m.appendReplacement(buf, Matcher.quoteReplacement(ch)) + } + m.appendTail(buf) + return buf.toString() + } + + fun byteToHexString(bytes: ByteArray?): String { + if (bytes == null) return "" + val sb = StringBuilder(bytes.size * 2) + for (b in bytes) { + val hex = 0xff and b.toInt() + if (hex < 16) { + sb.append('0') + } + sb.append(Integer.toHexString(hex)) + } + return sb.toString() + } + + fun hexStringToByte(hexString: String): ByteArray? { + val hexStr = hexString.replace(" ", "") + val len = hexStr.length + val bytes = ByteArray(len / 2) + var i = 0 + while (i < len) { + // 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节 + bytes[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + + Character.digit(hexString[i + 1], 16)).toByte() + i += 2 + } + return bytes + } + + fun convertCC(input: String?): String { + return when (AppConfig.chineseConverterType) { + 1 -> { + HanLP.convertToSimplifiedChinese(input) + } + 2 -> { + HanLP.convertToTraditionalChinese(input) + } + else -> { + input!! + } + } + } +} diff --git a/app/src/main/java/com/novel/read/utils/SystemUtils.kt b/app/src/main/java/com/novel/read/utils/SystemUtils.kt new file mode 100644 index 0000000..58114a8 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/SystemUtils.kt @@ -0,0 +1,73 @@ +package com.novel.read.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.content.Intent +import android.net.Uri +import android.os.PowerManager +import android.provider.Settings +import android.view.View +import android.view.ViewGroup + + +@Suppress("unused") +object SystemUtils { + + private const val NAVIGATION = "navigationBarBackground" + + fun getScreenOffTime(context: Context): Int { + var screenOffTime = 0 + try { + screenOffTime = Settings.System.getInt( + context.contentResolver, + Settings.System.SCREEN_OFF_TIMEOUT + ) + } catch (e: Exception) { + e.printStackTrace() + } + + return screenOffTime + } + + fun ignoreBatteryOptimization(activity: Activity) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return + + val powerManager = activity.getSystemService(POWER_SERVICE) as PowerManager + val hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.packageName) + // 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。 + if (!hasIgnored) { + try { + @SuppressLint("BatteryLife") + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + intent.data = Uri.parse("package:" + activity.packageName) + activity.startActivity(intent) + } catch (ignored: Throwable) { + } + + } + } + + /** + * 返回NavigationBar是否存在 + * 该方法需要在View完全被绘制出来之后调用,否则判断不了 + * 在比如 onWindowFocusChanged()方法中可以得到正确的结果 + */ + fun isNavigationBarExist(activity: Activity?): Boolean { + activity?.let { + val vp = it.window.decorView as? ViewGroup + if (vp != null) { + for (i in 0 until vp.childCount) { + vp.getChildAt(i).context.packageName + if (vp.getChildAt(i).id != View.NO_ID + && NAVIGATION == activity.resources.getResourceEntryName(vp.getChildAt(i).id) + ) { + return true + } + } + } + } + return false + } +} diff --git a/app/src/main/java/com/novel/read/utils/UIUtils.kt b/app/src/main/java/com/novel/read/utils/UIUtils.kt new file mode 100644 index 0000000..dc44340 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/UIUtils.kt @@ -0,0 +1,45 @@ +package com.novel.read.utils + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.Build +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import com.novel.read.R +import com.novel.read.constant.Theme +import com.novel.read.utils.ext.* + +@Suppress("unused") +object UIUtils { + + /** 设置更多工具条图标和颜色 */ + fun setToolbarMoreIconCustomColor(toolbar: Toolbar?, color: Int? = null) { + toolbar ?: return + val moreIcon = ContextCompat.getDrawable(toolbar.context, R.drawable.ic_launcher_background) + if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (color != null) { + moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + toolbar.overflowIcon = moreIcon + } + } + + + fun getMenuColor( + context: Context, + theme: Theme = Theme.Auto, + requiresOverflow: Boolean = false + ): Int { + val defaultTextColor = context.getCompatColor(R.color.primaryText) + if (requiresOverflow) + return defaultTextColor + val primaryTextColor = context.primaryTextColor + return when (theme) { + Theme.Dark -> context.getCompatColor(R.color.md_white_1000) + Theme.Light -> context.getCompatColor(R.color.md_black_1000) + else -> primaryTextColor + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ViewUtils.kt b/app/src/main/java/com/novel/read/utils/ViewUtils.kt new file mode 100644 index 0000000..31844e2 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ViewUtils.kt @@ -0,0 +1,42 @@ +package com.novel.read.utils + +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.TransitionDrawable +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.ColorInt + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused") +object ViewUtils { + + fun removeOnGlobalLayoutListener(v: View, listener: ViewTreeObserver.OnGlobalLayoutListener) { + v.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + + fun setBackgroundCompat(view: View, drawable: Drawable?) { + view.background = drawable + } + + fun setBackgroundTransition(view: View, newDrawable: Drawable): TransitionDrawable { + val transition = DrawableUtils.createTransitionDrawable(view.background, newDrawable) + setBackgroundCompat(view, transition) + return transition + } + + fun setBackgroundColorTransition(view: View, @ColorInt newColor: Int): TransitionDrawable { + val oldColor = view.background + + val start = oldColor ?: ColorDrawable(view.solidColor) + val end = ColorDrawable(newColor) + + val transition = DrawableUtils.createTransitionDrawable(start, end) + + setBackgroundCompat(view, transition) + + return transition + } +} diff --git a/app/src/main/java/com/novel/read/utils/ext/ActivityExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/ActivityExtensions.kt new file mode 100644 index 0000000..5ee36fa --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/ActivityExtensions.kt @@ -0,0 +1,32 @@ +package com.novel.read.utils.ext + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.util.DisplayMetrics +import android.view.WindowInsets +import android.view.WindowMetrics + + +inline fun Activity.startActivityForResult( + requestCode: Int, + configIntent: Intent.() -> Unit = {} +) { + startActivityForResult(Intent(this, A::class.java).apply(configIntent), requestCode) +} + + +fun Activity.getSize(): DisplayMetrics { + val displayMetrics = DisplayMetrics() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + val insets = windowMetrics.windowInsets + .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) + displayMetrics.widthPixels = windowMetrics.bounds.width() - insets.left - insets.right + displayMetrics.heightPixels = windowMetrics.bounds.height() - insets.top - insets.bottom + } else { + @Suppress("DEPRECATION") + windowManager.defaultDisplay.getMetrics(displayMetrics) + } + return displayMetrics +} diff --git a/app/src/main/java/com/novel/read/utils/ext/AlertDialogExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/AlertDialogExtensions.kt new file mode 100644 index 0000000..74846db --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/AlertDialogExtensions.kt @@ -0,0 +1,13 @@ +package com.novel.read.utils.ext + +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import com.novel.read.lib.ATH + +fun AlertDialog.applyTint(): AlertDialog { + return ATH.setAlertDialogTint(this) +} + +fun AlertDialog.requestInputMethod(){ + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) +} diff --git a/app/src/main/java/com/novel/read/utils/ext/ContextExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/ContextExtensions.kt new file mode 100644 index 0000000..b57c625 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/ContextExtensions.kt @@ -0,0 +1,206 @@ +@file:Suppress("unused") + +package com.novel.read.utils.ext + +import android.content.* +import android.content.pm.PackageManager +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.BatteryManager +import android.provider.Settings +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import com.novel.read.R +import org.jetbrains.anko.defaultSharedPreferences +import org.jetbrains.anko.longToast +import org.jetbrains.anko.toast +import java.io.File + +fun Context.getPrefBoolean(key: String, defValue: Boolean = false) = + defaultSharedPreferences.getBoolean(key, defValue) + +fun Context.getPrefBoolean(@StringRes keyId: Int, defValue: Boolean = false) = + defaultSharedPreferences.getBoolean(getString(keyId), defValue) + +fun Context.putPrefBoolean(key: String, value: Boolean = false) = + defaultSharedPreferences.edit { putBoolean(key, value) } + +fun Context.getPrefInt(key: String, defValue: Int = 0) = + defaultSharedPreferences.getInt(key, defValue) + +fun Context.putPrefInt(key: String, value: Int) = + defaultSharedPreferences.edit { putInt(key, value) } + +fun Context.getPrefLong(key: String, defValue: Long = 0L) = + defaultSharedPreferences.getLong(key, defValue) + +fun Context.putPrefLong(key: String, value: Long) = + defaultSharedPreferences.edit { putLong(key, value) } + +fun Context.getPrefString(key: String, defValue: String? = null) = + defaultSharedPreferences.getString(key, defValue) + +fun Context.getPrefString(@StringRes keyId: Int, defValue: String? = null) = + defaultSharedPreferences.getString(getString(keyId), defValue) + +fun Context.putPrefString(key: String, value: String?) = + defaultSharedPreferences.edit { putString(key, value) } + +fun Context.getPrefStringSet( + key: String, + defValue: MutableSet? = null +): MutableSet? = defaultSharedPreferences.getStringSet(key, defValue) + +fun Context.putPrefStringSet(key: String, value: MutableSet) = + defaultSharedPreferences.edit { putStringSet(key, value) } + +fun Context.removePref(key: String) = + defaultSharedPreferences.edit { remove(key) } + + +fun Context.getCompatColor(@ColorRes id: Int): Int = ContextCompat.getColor(this, id) + +fun Context.getCompatDrawable(@DrawableRes id: Int): Drawable? = ContextCompat.getDrawable(this, id) + +fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = + ContextCompat.getColorStateList(this, id) + +/** + * 系统息屏时间 + */ +val Context.sysScreenOffTime: Int + get() { + var screenOffTime = 0 + try { + screenOffTime = + Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT) + } catch (e: Exception) { + e.printStackTrace() + } + return screenOffTime + } + +val Context.statusBarHeight: Int + get() { + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + +val Context.navigationBarHeight: Int + get() { + val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + + +fun Context.sendToClip(text: String) { + val clipboard = + getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + val clipData = ClipData.newPlainText(null, text) + clipboard?.let { + clipboard.setPrimaryClip(clipData) + longToast(R.string.copy_complete) + } +} + +fun Context.getClipText(): String? { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? + clipboard?.primaryClip?.let { + if (it.itemCount > 0) { + return it.getItemAt(0).text.toString().trim() + } + } + return null +} + +fun Context.sendMail(mail: String) { + try { + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:$mail") + startActivity(intent) + } catch (e: Exception) { + toast(e.localizedMessage ?: "Error") + } +} + +/** + * 系统是否暗色主题 + */ +fun Context.sysIsDarkMode(): Boolean { + val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return mode == Configuration.UI_MODE_NIGHT_YES +} + +/** + * 获取电量 + */ +val Context.sysBattery: Int + get() { + val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus = registerReceiver(null, iFilter) + return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 + } + +val Context.externalFilesDir: File + get() = this.getExternalFilesDir(null) ?: this.filesDir + +val Context.eCacheDir: File + get() = this.externalCacheDir ?: this.cacheDir + +fun Context.openUrl(url: String) { + openUrl(Uri.parse(url)) +} + +fun Context.openUrl(uri: Uri) { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = uri + if (intent.resolveActivity(packageManager) != null) { + try { + startActivity(intent) + } catch (e: Exception) { + toast(e.localizedMessage ?: "open url error") + } + } else { + try { + startActivity(Intent.createChooser(intent, "请选择浏览器")) + } catch (e: Exception) { + toast(e.localizedMessage ?: "open url error") + } + } +} + +val Context.channel: String + get() { + try { + val pm = packageManager + val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + return appInfo.metaData.getString("channel") ?: "" + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + +fun Context.goShop(){ + val uri = Uri.parse("market://details?id=$packageName") + val goToMarket = Intent(Intent.ACTION_VIEW, uri) + val googlePlay = "com.android.vending"//这里对应的是谷歌商店,跳转别的商店改成对应的即可 + + goToMarket.setPackage(googlePlay)//这里对应的是谷歌商店,跳转别的商店改成对应的即可 + + try { + this.startActivity(goToMarket) + } catch (e: ActivityNotFoundException) { + if (goToMarket.resolveActivity(this.packageManager) != null) { //有浏览器 + this.startActivity(goToMarket) + } else { + toast(getString(R.string.no_google)) + } + e.printStackTrace() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/EventBusExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/EventBusExtensions.kt new file mode 100644 index 0000000..4320951 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/EventBusExtensions.kt @@ -0,0 +1,69 @@ +package com.novel.read.utils.ext + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import com.jeremyliao.liveeventbus.LiveEventBus +import com.jeremyliao.liveeventbus.core.Observable + +inline fun eventObservable(tag: String): Observable { + return LiveEventBus.get(tag, EVENT::class.java) +} + +inline fun postEvent(tag: String, event: EVENT) { + LiveEventBus.get(tag).post(event) +} + +inline fun postEventDelay(tag: String, event: EVENT, delay: Long) { + LiveEventBus.get(tag).postDelay(event, delay) +} + +inline fun AppCompatActivity.observeEvent( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observe(this, o) + } +} + + +inline fun AppCompatActivity.observeEventSticky( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observeSticky(this, o) + } +} + +inline fun Fragment.observeEvent( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observe(this, o) + } +} + +inline fun Fragment.observeEventSticky( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observeSticky(this, o) + } +} + diff --git a/app/src/main/java/com/novel/read/utils/ext/FloatExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/FloatExtensions.kt new file mode 100644 index 0000000..a50b329 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/FloatExtensions.kt @@ -0,0 +1,16 @@ +@file:Suppress("unused") + +package com.novel.read.utils.ext + +import android.content.res.Resources + +val Float.dp: Float + get() = android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics + ) + +val Float.sp: Float + get() = android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics + ) + diff --git a/app/src/main/java/com/novel/read/utils/ext/FragmentExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/FragmentExtensions.kt new file mode 100644 index 0000000..14e4629 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/FragmentExtensions.kt @@ -0,0 +1,85 @@ +@file:Suppress("unused") + +package com.novel.read.utils.ext + +import android.app.Activity +import android.app.Service +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.edit +import androidx.fragment.app.Fragment +import org.jetbrains.anko.connectivityManager +import org.jetbrains.anko.defaultSharedPreferences +import org.jetbrains.anko.internals.AnkoInternals + +@Suppress("DEPRECATION") +fun Fragment.isOnline() = + requireContext().connectivityManager.activeNetworkInfo?.isConnected == true + +fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) = + requireContext().defaultSharedPreferences.getBoolean(key, defValue) + +fun Fragment.putPrefBoolean(key: String, value: Boolean = false) = + requireContext().defaultSharedPreferences.edit { putBoolean(key, value) } + +fun Fragment.getPrefInt(key: String, defValue: Int = 0) = + requireContext().defaultSharedPreferences.getInt(key, defValue) + +fun Fragment.putPrefInt(key: String, value: Int) = + requireContext().defaultSharedPreferences.edit { putInt(key, value) } + +fun Fragment.getPrefLong(key: String, defValue: Long = 0L) = + requireContext().defaultSharedPreferences.getLong(key, defValue) + +fun Fragment.putPrefLong(key: String, value: Long) = + requireContext().defaultSharedPreferences.edit { putLong(key, value) } + +fun Fragment.getPrefString(key: String, defValue: String? = null) = + requireContext().defaultSharedPreferences.getString(key, defValue) + +fun Fragment.putPrefString(key: String, value: String) = + requireContext().defaultSharedPreferences.edit { putString(key, value) } + +fun Fragment.getPrefStringSet( + key: String, + defValue: MutableSet? = null +): MutableSet? = + requireContext().defaultSharedPreferences.getStringSet(key, defValue) + +fun Fragment.putPrefStringSet(key: String, value: MutableSet) = + requireContext().defaultSharedPreferences.edit { putStringSet(key, value) } + +fun Fragment.removePref(key: String) = + requireContext().defaultSharedPreferences.edit { remove(key) } + +fun Fragment.getCompatColor(@ColorRes id: Int): Int = requireContext().getCompatColor(id) + +fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? = + requireContext().getCompatDrawable(id) + +fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = + requireContext().getCompatColorStateList(id) + +inline fun Fragment.startActivity(vararg params: Pair) = + AnkoInternals.internalStartActivity(requireActivity(), T::class.java, params) + +inline fun Fragment.startActivityForResult( + requestCode: Int, + vararg params: Pair +) = + startActivityForResult( + AnkoInternals.createIntent(requireActivity(), T::class.java, params), + requestCode + ) + +inline fun Fragment.startService(vararg params: Pair) = + AnkoInternals.internalStartService(requireActivity(), T::class.java, params) + +inline fun Fragment.stopService(vararg params: Pair) = + AnkoInternals.internalStopService(requireActivity(), T::class.java, params) + +inline fun Fragment.intentFor(vararg params: Pair): Intent = + AnkoInternals.createIntent(requireActivity(), T::class.java, params) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/GsonExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/GsonExtensions.kt new file mode 100644 index 0000000..eeae03b --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/GsonExtensions.kt @@ -0,0 +1,138 @@ +package com.novel.read.utils.ext + +import com.google.gson.* +import com.google.gson.internal.LinkedTreeMap +import com.google.gson.reflect.TypeToken +import org.jetbrains.anko.attempt +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import kotlin.jvm.Throws +import kotlin.math.ceil + + +val GSON: Gson by lazy { + GsonBuilder() + .registerTypeAdapter( + object : TypeToken?>() {}.type, + MapDeserializerDoubleAsIntFix() + ) + .registerTypeAdapter(Int::class.java, IntJsonDeserializer()) + .disableHtmlEscaping() + .setPrettyPrinting() + .create() +} + +inline fun genericType(): Type = object : TypeToken() {}.type + +inline fun Gson.fromJsonObject(json: String?): T? {//可转成任意类型 + return attempt { + val result: T? = fromJson(json, genericType()) + result + }.value +} + +inline fun Gson.fromJsonArray(json: String?): List? { + return attempt { + val result: List? = fromJson(json, ParameterizedTypeImpl(T::class.java)) + result + }.value +} + +class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType { + override fun getRawType(): Type = List::class.java + + override fun getOwnerType(): Type? = null + + override fun getActualTypeArguments(): Array = arrayOf(clazz) +} + +/** + * int类型转化失败时跳过 + */ +class IntJsonDeserializer : JsonDeserializer { + + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Int? { + return when { + json.isJsonPrimitive -> { + val prim = json.asJsonPrimitive + if (prim.isNumber) { + prim.asNumber.toInt() + } else { + null + } + } + else -> null + } + } + +} + + +/** + * 修复Int变为Double的问题 + */ +class MapDeserializerDoubleAsIntFix : + JsonDeserializer?> { + + @Throws(JsonParseException::class) + override fun deserialize( + jsonElement: JsonElement, + type: Type, + jsonDeserializationContext: JsonDeserializationContext + ): Map? { + @Suppress("unchecked_cast") + return read(jsonElement) as? Map + } + + fun read(json: JsonElement): Any? { + when { + json.isJsonArray -> { + val list: MutableList = ArrayList() + val arr = json.asJsonArray + for (anArr in arr) { + list.add(read(anArr)) + } + return list + } + json.isJsonObject -> { + val map: MutableMap = + LinkedTreeMap() + val obj = json.asJsonObject + val entitySet = + obj.entrySet() + for ((key, value) in entitySet) { + map[key] = read(value) + } + return map + } + json.isJsonPrimitive -> { + val prim = json.asJsonPrimitive + when { + prim.isBoolean -> { + return prim.asBoolean + } + prim.isString -> { + return prim.asString + } + prim.isNumber -> { + val num: Number = prim.asNumber + // here you can handle double int/long values + // and return any type you want + // this solution will transform 3.0 float to long values + return if (ceil(num.toDouble()) == num.toLong().toDouble()) { + num.toLong() + } else { + num.toDouble() + } + } + } + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/IntExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/IntExtensions.kt new file mode 100644 index 0000000..87de5a6 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/IntExtensions.kt @@ -0,0 +1,16 @@ +package com.novel.read.utils.ext + +import android.content.res.Resources + +val Int.dp: Int + get() = android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics + ).toInt() + +val Int.sp: Int + get() = android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics + ).toInt() + +val Int.hexString: String + get() = Integer.toHexString(this) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/MaterialValueHelper.kt b/app/src/main/java/com/novel/read/utils/ext/MaterialValueHelper.kt new file mode 100644 index 0000000..9115692 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/MaterialValueHelper.kt @@ -0,0 +1,127 @@ +@file:Suppress("unused") + +package com.novel.read.utils.ext + +import android.annotation.SuppressLint +import android.content.Context +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.novel.read.R +import com.novel.read.lib.ThemeStore +import com.novel.read.utils.ColorUtils + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@SuppressLint("PrivateResource") +@ColorInt +fun Context.getPrimaryTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.primary_text_default_material_light) + } else ContextCompat.getColor(this, R.color.primary_text_default_material_dark) +} + +@SuppressLint("PrivateResource") +@ColorInt +fun Context.getSecondaryTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.secondary_text_default_material_light) + } else ContextCompat.getColor(this, R.color.secondary_text_default_material_dark) +} + +@SuppressLint("PrivateResource") +@ColorInt +fun Context.getPrimaryDisabledTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.primary_text_disabled_material_light) + } else ContextCompat.getColor(this, R.color.primary_text_disabled_material_dark) +} + +@SuppressLint("PrivateResource") +@ColorInt +fun Context.getSecondaryDisabledTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.secondary_text_disabled_material_light) + } else ContextCompat.getColor(this, R.color.secondary_text_disabled_material_dark) +} + +val Context.primaryColor: Int + get() = ThemeStore.primaryColor(this) + +val Context.primaryColorDark: Int + get() = ThemeStore.primaryColorDark(this) + +val Context.accentColor: Int + get() = ThemeStore.accentColor(this) + +val Context.backgroundColor: Int + get() = ThemeStore.backgroundColor(this) + +val Context.bottomBackground: Int + get() = ThemeStore.bottomBackground(this) + +val Context.primaryTextColor: Int + get() = getPrimaryTextColor(isDarkTheme) + +val Context.secondaryTextColor: Int + get() = getSecondaryTextColor(isDarkTheme) + +val Context.primaryDisabledTextColor: Int + get() = getPrimaryDisabledTextColor(isDarkTheme) + +val Context.secondaryDisabledTextColor: Int + get() = getSecondaryDisabledTextColor(isDarkTheme) + +val Fragment.primaryColor: Int + get() = ThemeStore.primaryColor(requireContext()) + +val Fragment.primaryColorDark: Int + get() = ThemeStore.primaryColorDark( + requireContext() + ) + +val Fragment.accentColor: Int + get() = ThemeStore.accentColor(requireContext()) + +val Fragment.backgroundColor: Int + get() = ThemeStore.backgroundColor( + requireContext() + ) + +val Fragment.bottomBackground: Int + get() = ThemeStore.bottomBackground( + requireContext() + ) + +val Fragment.primaryTextColor: Int + get() = requireContext().getPrimaryTextColor(isDarkTheme) + +val Fragment.secondaryTextColor: Int + get() = requireContext().getSecondaryTextColor(isDarkTheme) + +val Fragment.primaryDisabledTextColor: Int + get() = requireContext().getPrimaryDisabledTextColor(isDarkTheme) + +val Fragment.secondaryDisabledTextColor: Int + get() = requireContext().getSecondaryDisabledTextColor(isDarkTheme) + +val Context.buttonDisabledColor: Int + get() = if (isDarkTheme) { + ContextCompat.getColor(this, R.color.ate_button_disabled_dark) + } else { + ContextCompat.getColor(this, R.color.ate_button_disabled_light) + } + +val Context.isDarkTheme: Boolean + get() = ColorUtils.isColorLight( + ThemeStore.primaryColor( + this + ) + ) + +val Fragment.isDarkTheme: Boolean + get() = requireContext().isDarkTheme + +val Context.elevation: Float + get() = ThemeStore.elevation(this) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/MenuExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/MenuExtensions.kt new file mode 100644 index 0000000..757a84e --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/MenuExtensions.kt @@ -0,0 +1,61 @@ +package com.novel.read.utils.ext + +import android.annotation.SuppressLint +import android.content.Context +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.core.view.forEach +import com.novel.read.R +import com.novel.read.constant.Theme +import com.novel.read.utils.DrawableUtils +import com.novel.read.utils.UIUtils +import java.lang.reflect.Method +import java.util.* + +@SuppressLint("RestrictedApi") +fun Menu.applyTint(context: Context, theme: Theme = Theme.Auto): Menu = this.let { menu -> + if (menu is MenuBuilder) { + menu.setOptionalIconsVisible(true) + } + val defaultTextColor = context.getCompatColor(R.color.primaryText) + val tintColor = UIUtils.getMenuColor(context, theme) + menu.forEach { item -> + (item as MenuItemImpl).let { impl -> + //overflow:展开的item + DrawableUtils.setTint( + impl.icon, + if (impl.requiresOverflow()) defaultTextColor + else tintColor + ) + } + } + return menu +} + +fun Menu.applyOpenTint(context: Context) { + //展开菜单显示图标 + if (this.javaClass.simpleName.equals("MenuBuilder", ignoreCase = true)) { + val defaultTextColor = context.getCompatColor(R.color.primaryText) + try { + var method: Method = + this.javaClass.getDeclaredMethod("setOptionalIconsVisible", java.lang.Boolean.TYPE) + method.isAccessible = true + method.invoke(this, true) + method = this.javaClass.getDeclaredMethod("getNonActionItems") + val menuItems = method.invoke(this) + if (menuItems is ArrayList<*>) { + for (menuItem in menuItems) { + if (menuItem is MenuItem) { + DrawableUtils.setTint( + menuItem.icon, + defaultTextColor + ) + } + } + } + } catch (ignored: Exception) { + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/StringExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/StringExtensions.kt new file mode 100644 index 0000000..96da40d --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/StringExtensions.kt @@ -0,0 +1,86 @@ +@file:Suppress("unused") + +package com.novel.read.utils.ext + +import android.net.Uri +import java.io.File + +val removeHtmlRegex = "]*>".toRegex() +val imgRegex = "]*>".toRegex() +val notImgHtmlRegex = "]*>".toRegex() + +fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() + +fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true + +fun String.parseToUri(): Uri { + return if (isContentPath()) { + Uri.parse(this) + } else { + Uri.fromFile(File(this)) + } +} + +fun String?.isAbsUrl() = + this?.let { + it.startsWith("http://", true) + || it.startsWith("https://", true) + } ?: false + +fun String?.isJson(): Boolean = + this?.run { + val str = this.trim() + when { + str.startsWith("{") && str.endsWith("}") -> true + str.startsWith("[") && str.endsWith("]") -> true + else -> false + } + } ?: false + +fun String?.isJsonObject(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("{") && str.endsWith("}") + } ?: false + +fun String?.isJsonArray(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("[") && str.endsWith("]") + } ?: false + +fun String?.htmlFormat(): String { + this ?: return "" + return this + .replace(imgRegex, "\n$0\n") + .replace(removeHtmlRegex, "\n") + .replace(notImgHtmlRegex, "") + .replace("\\s*\\n+\\s*".toRegex(), "\n  ") + .replace("^[\\n\\s]+".toRegex(), "  ") + .replace("[\\n\\s]+$".toRegex(), "") +} + +fun String.splitNotBlank(vararg delimiter: String): Array = run { + this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() +} + +fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { + this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() +} + +/** + * 将字符串拆分为单个字符,包含emoji + */ +fun String.toStringArray(): Array { + var codePointIndex = 0 + return try { + Array(codePointCount(0, length)) { + val start = codePointIndex + codePointIndex = offsetByCodePoints(start, 1) + substring(start, codePointIndex) + } + } catch (e: Exception) { + split("").toTypedArray() + } +} + diff --git a/app/src/main/java/com/novel/read/utils/ext/Toasts.kt b/app/src/main/java/com/novel/read/utils/ext/Toasts.kt new file mode 100644 index 0000000..aed7fa2 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/Toasts.kt @@ -0,0 +1,37 @@ +@file:Suppress("unused") + +package com.novel.read.utils.ext + +import android.widget.Toast +import androidx.fragment.app.Fragment +import org.jetbrains.anko.longToast +import org.jetbrains.anko.toast + + +/** + * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. + * + * @param message the message text resource. + */ +fun Fragment.toast(message: Int) = requireActivity().toast(message) + +/** + * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. + * + * @param message the message text. + */ +fun Fragment.toast(message: CharSequence) = requireActivity().toast(message) + +/** + * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. + * + * @param message the message text resource. + */ +fun Fragment.longToast(message: Int) = requireActivity().longToast(message) + +/** + * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. + * + * @param message the message text. + */ +fun Fragment.longToast(message: CharSequence) = requireActivity().longToast(message) diff --git a/app/src/main/java/com/novel/read/utils/ext/ViewExtensions.kt b/app/src/main/java/com/novel/read/utils/ext/ViewExtensions.kt new file mode 100644 index 0000000..2f9b184 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/ViewExtensions.kt @@ -0,0 +1,119 @@ +package com.novel.read.utils.ext + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.Build +import android.view.View +import android.view.View.* +import android.view.inputmethod.InputMethodManager +import android.widget.RadioGroup +import android.widget.SeekBar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.menu.MenuPopupHelper +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.get +import com.novel.read.App +import java.lang.reflect.Field + + +private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? { + return when (context) { + is AppCompatActivity -> context + is androidx.appcompat.view.ContextThemeWrapper -> getCompatActivity(context.baseContext) + is android.view.ContextThemeWrapper -> getCompatActivity(context.baseContext) + else -> null + } +} + +val View.activity: AppCompatActivity? + get() = getCompatActivity(context) + +fun View.hideSoftInput() = run { + val imm = App.INSTANCE.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.let { + imm.hideSoftInputFromWindow(this.windowToken, 0) + } +} + +fun View.disableAutoFill() = run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS + } +} + +fun View.gone() { + if (visibility != GONE) { + visibility = GONE + } +} + +fun View.invisible() { + if (visibility != INVISIBLE) { + visibility = INVISIBLE + } +} + +fun View.visible() { + if (visibility != VISIBLE) { + visibility = VISIBLE + } +} + +fun View.visible(visible: Boolean) { + if (visible && visibility != VISIBLE) { + visibility = VISIBLE + } else if (!visible && visibility == VISIBLE) { + visibility = INVISIBLE + } +} + +fun View.screenshot(): Bitmap? { + return runCatching { + val screenshot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val c = Canvas(screenshot) + c.translate(-scrollX.toFloat(), -scrollY.toFloat()) + draw(c) + screenshot + }.getOrNull() +} + +fun SeekBar.progressAdd(int: Int) { + progress += int +} + +fun RadioGroup.getIndexById(id: Int): Int { + for (i in 0 until this.childCount) { + if (id == get(i).id) { + return i + } + } + return 0 +} + +fun RadioGroup.getCheckedIndex(): Int { + for (i in 0 until this.childCount) { + if (checkedRadioButtonId == get(i).id) { + return i + } + } + return 0 +} + +fun RadioGroup.checkByIndex(index: Int) { + check(get(index).id) +} + +@SuppressLint("RestrictedApi") +fun PopupMenu.show(x: Int, y: Int) { + try { + val field: Field = this.javaClass.getDeclaredField("mPopup") + field.isAccessible = true + (field.get(this) as MenuPopupHelper).show(x, y) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utils/ext/ViewModelKt.kt b/app/src/main/java/com/novel/read/utils/ext/ViewModelKt.kt new file mode 100644 index 0000000..918d094 --- /dev/null +++ b/app/src/main/java/com/novel/read/utils/ext/ViewModelKt.kt @@ -0,0 +1,18 @@ +package com.novel.read.utils.ext + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +fun AppCompatActivity.getViewModel(clazz: Class) = + ViewModelProvider(this).get(clazz) + +fun Fragment.getViewModel(clazz: Class) = + ViewModelProvider(this).get(clazz) + +/** + * 与activity数据同步 + */ +fun Fragment.getViewModelOfActivity(clazz: Class) = + ViewModelProvider(requireActivity()).get(clazz) \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utlis/BrightnessUtils.kt b/app/src/main/java/com/novel/read/utlis/BrightnessUtils.kt deleted file mode 100644 index 10b43e1..0000000 --- a/app/src/main/java/com/novel/read/utlis/BrightnessUtils.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.novel.read.utlis - -import android.app.Activity -import android.content.ContentResolver -import android.provider.Settings -import android.util.Log -import android.view.WindowManager - -/** - * Created by zlj - * 调节亮度的工具类 - */ - -object BrightnessUtils { - private val TAG = "BrightnessUtils" - - /** - * 判断是否开启了自动亮度调节 - */ - fun isAutoBrightness(activity: Activity): Boolean { - var isAuto = false - try { - isAuto = Settings.System.getInt( - activity.contentResolver, - Settings.System.SCREEN_BRIGHTNESS_MODE - ) == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC - } catch (e: Settings.SettingNotFoundException) { - e.printStackTrace() - } - - return isAuto - } - - /** - * 获取屏幕的亮度 - * 系统亮度模式中,自动模式与手动模式获取到的系统亮度的值不同 - */ - fun getScreenBrightness(activity: Activity): Int { - return if (isAutoBrightness(activity)) { - getAutoScreenBrightness(activity) - } else { - getManualScreenBrightness(activity) - } - } - - /** - * 获取手动模式下的屏幕亮度 - * @return value:0~255 - */ - fun getManualScreenBrightness(activity: Activity): Int { - var nowBrightnessValue = 0 - val resolver = activity.contentResolver - try { - nowBrightnessValue = Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS) - } catch (e: Exception) { - e.printStackTrace() - } - - return nowBrightnessValue - } - - /** - * 获取自动模式下的屏幕亮度 - * @return value:0~255 - */ - fun getAutoScreenBrightness(activity: Activity): Int { - var nowBrightnessValue = 0f - - //获取自动调节下的亮度范围在 0~1 之间 - val resolver = activity.contentResolver - try { - nowBrightnessValue = - Settings.System.getFloat(resolver, Settings.System.SCREEN_BRIGHTNESS) - Log.d(TAG, "getAutoScreenBrightness: $nowBrightnessValue") - } catch (e: Exception) { - e.printStackTrace() - } - - //转换范围为 (0~255) - val fValue = nowBrightnessValue * 225.0f - Log.d(TAG, "brightness: $fValue") - return fValue.toInt() - } - - /** - * 设置亮度:通过设置 Windows 的 screenBrightness 来修改当前 Windows 的亮度 - * lp.screenBrightness:参数范围为 0~1 - */ - fun setBrightness(activity: Activity, brightness: Int) { - try { - val lp = activity.window.attributes - //将 0~255 范围内的数据,转换为 0~1 - lp.screenBrightness = java.lang.Float.valueOf(brightness.toFloat()) * (1f / 255f) - Log.d(TAG, "lp.screenBrightness == " + lp.screenBrightness) - activity.window.attributes = lp - } catch (ex: Exception) { - ex.printStackTrace() - } - - } - - /** - * 获取当前系统的亮度 - * @param activity - */ - fun setDefaultBrightness(activity: Activity) { - try { - val lp = activity.window.attributes - lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE - activity.window.attributes = lp - } catch (ex: Exception) { - ex.printStackTrace() - } - - } -} diff --git a/app/src/main/java/com/novel/read/utlis/Charset.kt b/app/src/main/java/com/novel/read/utlis/Charset.kt deleted file mode 100644 index ba67d99..0000000 --- a/app/src/main/java/com/novel/read/utlis/Charset.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.novel.read.utlis - -/** - * 编码类型 - */ -enum class Charset private constructor(var code: String) { - UTF8("UTF-8"), - UTF16LE("UTF-16LE"), - UTF16BE("UTF-16BE"), - GBK("GBK"); - - - companion object { - val BLANK: Byte = 0x0a - } -} diff --git a/app/src/main/java/com/novel/read/utlis/CleanCacheUtils.kt b/app/src/main/java/com/novel/read/utlis/CleanCacheUtils.kt deleted file mode 100644 index 0e15a1b..0000000 --- a/app/src/main/java/com/novel/read/utlis/CleanCacheUtils.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.novel.read.utlis - -import android.content.Context -import android.os.Environment - -import com.novel.read.model.db.BookChapterBean -import com.novel.read.model.db.CollBookBean -import com.novel.read.model.db.SearchListTable - -import org.litepal.LitePal - -import java.io.File -import java.math.BigDecimal - -/** - * @author: LiJun 390057892@qq.com - * @date: 2018/4/11 16:00 - */ - -class CleanCacheUtils { - - /** - * @param context - * @return - * @throws Exception - * 获取当前缓存 - */ - @Throws(Exception::class) - fun getTotalCacheSize(context: Context): String { - var cacheSize = getFolderSize(context.cacheDir) - if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { - cacheSize += getFolderSize(context.externalCacheDir) - } - return getFormatSize(cacheSize.toDouble()) - } - - - /** - * 清除缓存 - * - * @param clearReadPos 是否删除阅读记录 - */ - @Synchronized - fun clearCache(clearReadPos: Boolean, clearCollect: Boolean, context: Context) { - try { - clearAllCache(context) - // 删除搜索记录(SharePreference) - if (clearReadPos) { - LitePal.deleteAll(SearchListTable::class.java) - } - // 清空书架 - if (clearCollect) { - LitePal.deleteAll(CollBookBean::class.java) - LitePal.deleteAll(BookChapterBean::class.java) - } - } catch (e: Exception) { - } - - } - - companion object { - - private var instance: CleanCacheUtils? = null - - @Synchronized - fun getInstance(): CleanCacheUtils { - if (instance == null) { - instance = CleanCacheUtils() - } - return instance as CleanCacheUtils - } - - /** - * @param context - * 删除缓存 - */ - fun clearAllCache(context: Context) { - deleteDir(context.cacheDir) - if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { - deleteDir(context.externalCacheDir) - } - } - - private fun deleteDir(dir: File?): Boolean { - if (dir != null && dir.isDirectory) { - val children = dir.list() - var size = 0 - if (children != null) { - size = children.size - for (i in 0 until size) { - val success = deleteDir(File(dir, children[i])) - if (!success) { - return false - } - } - } - - } - return dir?.delete() ?: true - } - - // 获取文件 - // Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/ - // 目录,一般放一些长时间保存的数据 - // Context.getExternalCacheDir() --> - // SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据 - @Throws(Exception::class) - fun getFolderSize(file: File?): Long { - var size: Long = 0 - try { - val fileList = file!!.listFiles() - var size2 = 0 - if (fileList != null) { - size2 = fileList.size - for (i in 0 until size2) { - // 如果下面还有文件 - if (fileList[i].isDirectory) { - size = size + getFolderSize(fileList[i]) - } else { - size = size + fileList[i].length() - } - } - } - - } catch (e: Exception) { - e.printStackTrace() - } - - return size - } - - /** - * 格式化单位 - * 计算缓存的大小 - * @param size - * @return - */ - fun getFormatSize(size: Double): String { - val kiloByte = size / 1024 - if (kiloByte < 1) { - // return size + "Byte"; - return "0K" - } - - val megaByte = kiloByte / 1024 - if (megaByte < 1) { - val result1 = BigDecimal(java.lang.Double.toString(kiloByte)) - return result1.setScale(2, BigDecimal.ROUND_HALF_UP) - .toPlainString() + "KB" - } - - val gigaByte = megaByte / 1024 - if (gigaByte < 1) { - val result2 = BigDecimal(java.lang.Double.toString(megaByte)) - return result2.setScale(2, BigDecimal.ROUND_HALF_UP) - .toPlainString() + "MB" - } - - val teraBytes = gigaByte / 1024 - if (teraBytes < 1) { - val result3 = BigDecimal(java.lang.Double.toString(gigaByte)) - return result3.setScale(2, BigDecimal.ROUND_HALF_UP) - .toPlainString() + "GB" - } - val result4 = BigDecimal(teraBytes) - return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB" - } - } - -} diff --git a/app/src/main/java/com/novel/read/utlis/DateUtli.kt b/app/src/main/java/com/novel/read/utlis/DateUtli.kt deleted file mode 100644 index 561130d..0000000 --- a/app/src/main/java/com/novel/read/utlis/DateUtli.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.novel.read.utlis - -import android.annotation.SuppressLint - - -import com.novel.read.constants.Constant - -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date - -@SuppressLint("SimpleDateFormat") -object DateUtli { - - private val HOUR_OF_DAY = 24 - private val DAY_OF_YESTERDAY = 2 - private val TIME_UNIT = 60 - - /** - * 时间戳格式转换 - */ - private val dayNames = arrayOf("周日", "周一", "周二", "周三", "周四", "周五", "周六") - - //将时间转换成日期 - fun dateConvert(time: Long, pattern: String): String { - val date = Date(time) - @SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) - return format.format(date) - } - - - fun dateConvert(timesamp: Long, flag: Int): String { - var time = timesamp - time *= 1000 - val result: String - val todayCalendar = Calendar.getInstance() - val otherCalendar = Calendar.getInstance() - otherCalendar.timeInMillis = time - - val timeFormat = "M月d日" - val yearTimeFormat = "yyyy年M月d日" - - val yearTemp = todayCalendar.get(Calendar.YEAR) == otherCalendar.get(Calendar.YEAR) - if (yearTemp) { - val todayMonth = todayCalendar.get(Calendar.MONTH) - val otherMonth = otherCalendar.get(Calendar.MONTH) - if (todayMonth == otherMonth) {//表示是同一个月 - when (todayCalendar.get(Calendar.DATE) - otherCalendar.get(Calendar.DATE)) { - 0 -> result = getHourAndMin(time) - 1 -> if (flag == 1) { - result = "昨天 " - } else { - result = "昨天 " + getHourAndMin(time) - } - 2, 3, 4, 5, 6 -> { - val dayOfMonth = otherCalendar.get(Calendar.WEEK_OF_MONTH) - val todayOfMonth = todayCalendar.get(Calendar.WEEK_OF_MONTH) - if (dayOfMonth == todayOfMonth) {//表示是同一周 - val dayOfWeek = otherCalendar.get(Calendar.DAY_OF_WEEK) - if (dayOfWeek != 1) {//判断当前是不是星期日 如想显示为:周日 12:09 可去掉此判断 - result = dayNames[otherCalendar.get(Calendar.DAY_OF_WEEK) - 1] - } else { - result = getTime(time, timeFormat) - } - } else { - result = getTime(time, timeFormat) - } - } - else -> result = getTime(time, timeFormat) - } - } else { - result = getTime(time, timeFormat) - } - } else { - result = getYearTime(time, yearTimeFormat) - } - return result - } - - /** - * 当天的显示时间格式 - */ - private fun getHourAndMin(time: Long): String { - val format = SimpleDateFormat("HH:mm") - return format.format(Date(time)) - } - - /** - * 不同一周的显示时间格式 - */ - private fun getTime(time: Long, timeFormat: String): String { - val format = SimpleDateFormat(timeFormat) - return format.format(Date(time)) - } - - /** - * 不同年的显示时间格式 - */ - private fun getYearTime(time: Long, yearTimeFormat: String): String { - val format = SimpleDateFormat(yearTimeFormat) - return format.format(Date(time)) - } - -} diff --git a/app/src/main/java/com/novel/read/utlis/DialogUtils.kt b/app/src/main/java/com/novel/read/utlis/DialogUtils.kt deleted file mode 100644 index 09e3b1a..0000000 --- a/app/src/main/java/com/novel/read/utlis/DialogUtils.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.novel.read.utlis - -import android.app.AlertDialog -import android.content.Context -import android.content.DialogInterface - -/** - * create by zlj on 2019/6/19 - * describe: - */ -class DialogUtils private constructor() { - - fun showAlertDialog( - context: Context, - msg: String, - dialogListener: DialogInterface.OnClickListener - ) { - // context = context.getApplicationContext(); - val builder = AlertDialog.Builder(context) - builder.setTitle("操作提示").setMessage(msg).setCancelable(false) - .setPositiveButton("确定", dialogListener) - .setNegativeButton("取消") { dialog, id -> dialog.cancel() } - val alert = builder.create() - alert.show() - } - - companion object { - - private var instance: DialogUtils? = null - - @Synchronized - fun getInstance(): DialogUtils { - if (instance == null) { - instance = DialogUtils() - } - return instance as DialogUtils - } - } -} diff --git a/app/src/main/java/com/novel/read/utlis/EvenItemDecoration.kt b/app/src/main/java/com/novel/read/utlis/EvenItemDecoration.kt deleted file mode 100644 index 0b20517..0000000 --- a/app/src/main/java/com/novel/read/utlis/EvenItemDecoration.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.novel.read.utlis - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -/** - * 设置adapter padding - */ -class EvenItemDecoration(private val space: Int, private val column: Int) : RecyclerView.ItemDecoration() { - override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { - val position = parent.getChildAdapterPosition(view) - // 每个span分配的间隔大小 - val spanSpace = space * (column +1) / column - // 列索引 - val colIndex = position % column - // 列左、右间隙 - outRect.left = space * (colIndex + 1) - spanSpace * colIndex - outRect.right = spanSpace * (colIndex + 1) - space * (colIndex + 1) - // 行间距 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/utlis/FileUtils.java b/app/src/main/java/com/novel/read/utlis/FileUtils.java deleted file mode 100644 index e2d511c..0000000 --- a/app/src/main/java/com/novel/read/utlis/FileUtils.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.novel.read.utlis; - -import android.os.Environment; - -import com.novel.read.base.MyApp; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.List; - -import io.reactivex.Single; -import io.reactivex.SingleEmitter; -import io.reactivex.SingleOnSubscribe; - -/** - * Created by zlj - */ - -public class FileUtils { - //采用自己的格式去设置文件,防止文件被系统文件查询到 - public static final String SUFFIX_NB = ".zlj"; - public static final String SUFFIX_TXT = ".txt"; - public static final String SUFFIX_EPUB = ".epub"; - public static final String SUFFIX_PDF = ".pdf"; - - //获取文件夹 - public static File getFolder(String filePath){ - File file = new File(filePath); - //如果文件夹不存在,就创建它 - if (!file.exists()){ - file.mkdirs(); - } - return file; - } - - //获取文件 - public static synchronized File getFile(String filePath){ - File file = new File(filePath); - try { - if (!file.exists()){ - //创建父类文件夹 - getFolder(file.getParent()); - //创建文件 - file.createNewFile(); - } - } catch (IOException e) { - e.printStackTrace(); - } - return file; - } - - //获取Cache文件夹 - public static String getCachePath(){ - if (isSdCardExist()){ - return MyApp.Companion.getContext() - .getExternalCacheDir() - .getAbsolutePath(); - } - else{ - return MyApp.Companion.getContext() - .getCacheDir() - .getAbsolutePath(); - } - } - - public static long getDirSize(File file){ - //判断文件是否存在 - if (file.exists()) { - //如果是目录则递归计算其内容的总大小 - if (file.isDirectory()) { - File[] children = file.listFiles(); - long size = 0; - for (File f : children) - size += getDirSize(f); - return size; - } else { - return file.length(); - } - } else { - return 0; - } - } - - public static String getFileSize(long size) { - if (size <= 0) return "0"; - final String[] units = new String[]{"b", "kb", "M", "G", "T"}; - //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 - int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); - //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 - return new DecimalFormat("#,##0.##").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; - } - - /** - * 本来是获取File的内容的。但是为了解决文本缩进、换行的问题 - * 这个方法就是专门用来获取书籍的... - * - * 应该放在BookRepository中。。。 - * @param file - * @return - */ - public static String getFileContent(File file){ - Reader reader = null; - String str = null; - StringBuilder sb = new StringBuilder(); - try { - reader = new FileReader(file); - BufferedReader br = new BufferedReader(reader); - while ((str = br.readLine()) != null){ - //过滤空语句 - if (!str.equals("")){ - //由于sb会自动过滤\n,所以需要加上去 - sb.append(" "+str+"\n"); - } - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - }finally { - IOUtils.INSTANCE.close(reader); - } - return sb.toString(); - } - - //判断是否挂载了SD卡 - private static boolean isSdCardExist(){ - if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){ - return true; - } - return false; - } - - //递归删除文件夹下的数据 - public static synchronized void deleteFile(String filePath){ - File file = new File(filePath); - if (!file.exists()) return; - - if (file.isDirectory()){ - File[] files = file.listFiles(); - for (File subFile : files){ - String path = subFile.getPath(); - deleteFile(path); - } - } - //删除文件 - file.delete(); - } - - //由于递归的耗时问题,取巧只遍历内部三层 - - //获取txt文件 - public static List getTxtFiles(String filePath, int layer){ - final List txtFiles = new ArrayList(); - File file = new File(filePath); - - //如果层级为 3,则直接返回 - if (layer == 3){ - return txtFiles; - } - - //获取文件夹 - File[] dirs = file.listFiles( - new FileFilter() { - @Override - public boolean accept(File pathname) { - if (pathname.isDirectory() && !pathname.getName().startsWith(".")) { - return true; - } - //获取txt文件 - else if (pathname.getName().endsWith(".txt")) { - txtFiles.add(pathname); - return false; - } else { - return false; - } - } - } - ); - //遍历文件夹 - for (File dir : dirs){ - //递归遍历txt文件 - txtFiles.addAll(getTxtFiles(dir.getPath(),layer + 1)); - } - return txtFiles; - } - - //由于遍历比较耗时 - public static Single> getSDTxtFile(){ - //外部存储卡路径 - final String rootPath = Environment.getExternalStorageDirectory().getPath(); - return Single.create(new SingleOnSubscribe>() { - @Override - public void subscribe(SingleEmitter> e) throws Exception { - List files = getTxtFiles(rootPath,0); - e.onSuccess(files); - } - }); - } - - //获取文件的编码格式 - public static Charset getCharset(String fileName) { - BufferedInputStream bis = null; - Charset charset = Charset.GBK; - byte[] first3Bytes = new byte[3]; - try { - boolean checked = false; - bis = new BufferedInputStream(new FileInputStream(fileName)); - bis.mark(0); - int read = bis.read(first3Bytes, 0, 3); - if (read == -1) - return charset; - if (first3Bytes[0] == (byte) 0xEF - && first3Bytes[1] == (byte) 0xBB - && first3Bytes[2] == (byte) 0xBF) { - charset = Charset.UTF8; - checked = true; - } - - bis.mark(0); - if (!checked) { - while ((read = bis.read()) != -1) { - if (read >= 0xF0) - break; - if (0x80 <= read && read <= 0xBF) // 单独出现BF以下的,也算是GBK - break; - if (0xC0 <= read && read <= 0xDF) { - read = bis.read(); - if (0x80 <= read && read <= 0xBF) // 双字节 (0xC0 - 0xDF) - // (0x80 - 0xBF),也可能在GB编码内 - continue; - else - break; - } else if (0xE0 <= read) {// 也有可能出错,但是几率较小 - read = bis.read(); - if (0x80 <= read && read <= 0xBF) { - read = bis.read(); - if (0x80 <= read && read <= 0xBF) { - charset = Charset.UTF8; - break; - } else - break; - } else - break; - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - IOUtils.INSTANCE.close(bis); - } - return charset; - } - - public static boolean isSdCardAvailable() { - return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); - } -} diff --git a/app/src/main/java/com/novel/read/utlis/GlideImageLoader.kt b/app/src/main/java/com/novel/read/utlis/GlideImageLoader.kt deleted file mode 100644 index 7feca84..0000000 --- a/app/src/main/java/com/novel/read/utlis/GlideImageLoader.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.novel.read.utlis - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.drawable.Drawable -import android.widget.ImageView - -import androidx.annotation.DrawableRes - -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.RoundedCorners -import com.bumptech.glide.request.RequestOptions -import com.novel.read.R - - -/** - * create by zlj on 2018/11/21 - * describe: - */ -object GlideImageLoader { - - private//设置错误图片 - //设置占位图片 - val requestHeadOptions: RequestOptions - get() = RequestOptions() - .error(R.mipmap.ic_launcher) - .placeholder(R.mipmap.ic_launcher) - .diskCacheStrategy(DiskCacheStrategy.ALL) - - fun displayImage(context: Context, path: String, imageView: ImageView) { - Glide.with(context) - .load(path) - .into(imageView) - } - - //加载矩形圆角图片 - fun displayCornerImage(context: Context, path: String, imageView: ImageView) { - //设置图片圆角角度 - val roundedCorners = RoundedCorners(10) - val options = RequestOptions.bitmapTransform(roundedCorners) - .placeholder(R.drawable.cover_default) - .error(R.drawable.cover_default) - - - Glide.with(context) - .load(path) - .apply(options) - .into(imageView) - - } - - - - //加载矩形圆角图片 自动设置占位图 - fun displayCornerImage(context: Context, path: String, imageView: ImageView, defaultId: Int) { - //设置图片圆角角度 - val roundedCorners = RoundedCorners(15) - val options = RequestOptions.bitmapTransform(roundedCorners) - .placeholder(defaultId) - .error(defaultId) - Glide.with(context) - .load(path) - .apply(options) - .into(imageView) - - } - - //加载圆形图片 - @SuppressLint("CheckResult") - fun loadCirCleImage( - context: Context, - path: String, - imageView: ImageView, - width: Int, - height: Int - ) { - val myOptions = requestHeadOptions.circleCrop() - myOptions.error(R.mipmap.ic_launcher) - myOptions.placeholder(R.mipmap.ic_launcher) - Glide.with(context.applicationContext) - .load(path) - .apply(myOptions) - .into(imageView) - } - -} diff --git a/app/src/main/java/com/novel/read/utlis/IOUtils.kt b/app/src/main/java/com/novel/read/utlis/IOUtils.kt deleted file mode 100644 index 12283c3..0000000 --- a/app/src/main/java/com/novel/read/utlis/IOUtils.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.novel.read.utlis - -import java.io.Closeable -import java.io.IOException - -/** - * Created by zlj - */ - -object IOUtils { - - fun close(closeable: Closeable?) { - if (closeable == null) return - try { - closeable.close() - } catch (e: IOException) { - e.printStackTrace() - //close error - } - - } -} diff --git a/app/src/main/java/com/novel/read/utlis/LocalManageUtil.java b/app/src/main/java/com/novel/read/utlis/LocalManageUtil.java deleted file mode 100644 index 354e97b..0000000 --- a/app/src/main/java/com/novel/read/utlis/LocalManageUtil.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.novel.read.utlis; - -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.os.Build; -import android.os.LocaleList; -import android.util.DisplayMetrics; -import android.util.Log; - -import com.novel.read.R; -import com.novel.read.widget.page.ReadSettingManager; - -import java.util.Locale; - -public class LocalManageUtil { - - private static final String TAG = "LocalManageUtil"; - - /** - * 获取系统的locale - * - * @return Locale对象 - */ - public static Locale getSystemLocale(Context context) { - return SpUtil.getInstance().getSystemCurrentLocal(); - } - - public static String getSelectLanguage(Context context) { - switch (SpUtil.getInstance().getSelectLanguage()) { - case 0: - return context.getString(R.string.a_simple); - case 1: - return context.getString(R.string.a_traditional); - default: - return context.getString(R.string.a_simple); - } - } - - /** - * 获取选择的语言设置 - * @param context context - */ - private static Locale getSetLanguageLocale(Context context) { - - switch (SpUtil.getInstance().getSelectLanguage()) { - - case 0: - return Locale.CHINA; - case 1: - return Locale.TAIWAN; - case 2: - default: - return Locale.ENGLISH; - } - } - - public static void saveSelectLanguage(Context context, int select) { - SpUtil.getInstance().saveLanguage(select); //本地国际化 - ReadSettingManager.Companion.getInstance().setConvertType(select); //网络数据国际化 - setApplicationLanguage(context); - } - - public static Context setLocal(Context context) { - return updateResources(context, getSetLanguageLocale(context)); - } - - private static Context updateResources(Context context, Locale locale) { - Locale.setDefault(locale); - - Resources res = context.getResources(); - Configuration config = new Configuration(res.getConfiguration()); - if (Build.VERSION.SDK_INT >= 17) { - config.setLocale(locale); - context = context.createConfigurationContext(config); - } else { - config.locale = locale; - res.updateConfiguration(config, res.getDisplayMetrics()); - } - return context; - } - - /** - * 设置语言类型 - */ - public static void setApplicationLanguage(Context context) { - Resources resources = context.getApplicationContext().getResources(); - DisplayMetrics dm = resources.getDisplayMetrics(); - Configuration config = resources.getConfiguration(); - Locale locale = getSetLanguageLocale(context); - config.locale = locale; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - LocaleList localeList = new LocaleList(locale); - LocaleList.setDefault(localeList); - config.setLocales(localeList); - context.getApplicationContext().createConfigurationContext(config); - Locale.setDefault(locale); - } - resources.updateConfiguration(config, dm); - } - - public static void saveSystemCurrentLanguage(Context context) { - Locale locale; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - locale = LocaleList.getDefault().get(0); - } else { - locale = Locale.getDefault(); - } - Log.d(TAG, locale.getLanguage()); - SpUtil.getInstance().setSystemCurrentLocal(locale); - } - - public static void onConfigurationChanged(Context context){ - saveSystemCurrentLanguage(context); - setLocal(context); - setApplicationLanguage(context); - } -} diff --git a/app/src/main/java/com/novel/read/utlis/MD5Utils.kt b/app/src/main/java/com/novel/read/utlis/MD5Utils.kt deleted file mode 100644 index 999e94f..0000000 --- a/app/src/main/java/com/novel/read/utlis/MD5Utils.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.novel.read.utlis - -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import kotlin.experimental.and - -/** - * 将字符串转化为MD5 - */ - -object MD5Utils { - - private fun strToMd5By32(str: String): String? { - var reStr: String? = null - try { - val md5 = MessageDigest.getInstance("MD5") - val bytes = md5.digest(str.toByteArray()) - val stringBuffer = StringBuilder() - for (b in bytes) { - val bt = b and 0xff.toByte() - if (bt < 16) { - stringBuffer.append(0) - } - stringBuffer.append(Integer.toHexString(bt.toInt())) - } - reStr = stringBuffer.toString() - } catch (e: NoSuchAlgorithmException) { - e.printStackTrace() - } - - return reStr - } - - fun strToMd5By16(str: String): String? { - var reStr = strToMd5By32(str) - if (reStr != null) { - reStr = reStr.substring(8, 24) - } - return reStr - } -} diff --git a/app/src/main/java/com/novel/read/utlis/MyTimeTask.kt b/app/src/main/java/com/novel/read/utlis/MyTimeTask.kt deleted file mode 100644 index 88012c6..0000000 --- a/app/src/main/java/com/novel/read/utlis/MyTimeTask.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.novel.read.utlis - -import java.util.* - -/** - * create by zlj on 2019/7/15 - * describe: - */ -class MyTimeTask(private val time: Long, private val task: TimerTask?) { - - private var timer: Timer? = null - - init { - if (timer == null) { - timer = Timer() - } - } - - fun start() { - timer!!.schedule(task, 0, time)//每隔time时间段就执行一次 - } - - fun stop() { - if (timer != null) { - timer!!.cancel() - task?.cancel() - } - } -} diff --git a/app/src/main/java/com/novel/read/utlis/NetworkUtils.kt b/app/src/main/java/com/novel/read/utlis/NetworkUtils.kt deleted file mode 100644 index fe78bf9..0000000 --- a/app/src/main/java/com/novel/read/utlis/NetworkUtils.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.novel.read.utlis - -import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkInfo - -import com.novel.read.base.MyApp - -object NetworkUtils { - - /** - * 获取活动网络信息 - * @return NetworkInfo - */ - private val networkInfo: NetworkInfo? - get() { - val cm = MyApp.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return cm.activeNetworkInfo - } - - /** - * 网络是否可用 - * @return - */ - val isAvailable: Boolean - get() { - val info = networkInfo - return info != null && info.isAvailable - } - - /** - * 网络是否连接 - * @return - */ - val isConnected: Boolean - get() { - val info = networkInfo - return info != null && info.isConnected - } - - /** - * 判断wifi是否连接状态 - * - * 需添加权限 `` - * - * @param context 上下文 - * @return `true`: 连接

`false`: 未连接 - */ - fun isWifiConnected(context: Context): Boolean { - val cm = context - .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return (cm.activeNetworkInfo != null - && cm.activeNetworkInfo.type == ConnectivityManager.TYPE_WIFI) - } - - -} diff --git a/app/src/main/java/com/novel/read/utlis/PermissionUtil.java b/app/src/main/java/com/novel/read/utlis/PermissionUtil.java deleted file mode 100644 index bceed44..0000000 --- a/app/src/main/java/com/novel/read/utlis/PermissionUtil.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.novel.read.utlis; - -import android.Manifest; -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.pm.PackageManager; -import android.os.Build; - -import com.novel.read.R; - -import java.util.ArrayList; -import java.util.List; - -public class PermissionUtil { - private static PermissionUtil permissionUtil = null; - private static List mListPermissions; - - - private static String[] PERMISSIONS_LIST = { - Manifest.permission.INTERNET, - Manifest.permission.ACCESS_NETWORK_STATE, - Manifest.permission.ACCESS_WIFI_STATE, - Manifest.permission.ACCESS_NETWORK_STATE, - }; - - private static final String PERMISSIONS_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE; - private static final String PERMISSIONS_PHONE = Manifest.permission.READ_PHONE_STATE; - private static final String PERMISSIONS_SETTINGS = Manifest.permission.WRITE_SETTINGS; - - - private void addAllPermissions(List mListPermissions) { - mListPermissions.add(PERMISSIONS_STORAGE); - } - - - public static PermissionUtil getInstance() { - if (permissionUtil == null) { - permissionUtil = new PermissionUtil(); - } - return permissionUtil; - } - - public boolean isOverMarshmallow() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - } - - @TargetApi(value = Build.VERSION_CODES.M) - public List findDeniedPermissions(Activity activity, List permissions) { - List denyPermissions = new ArrayList<>(); - for (String value : permissions) { - if (activity.checkSelfPermission(value) != PackageManager.PERMISSION_GRANTED) { - denyPermissions.add(value); - } - } - return denyPermissions; - } - - @TargetApi(value = Build.VERSION_CODES.M) - public void requestPermissions(Activity activity, int requestCode, PermissionCallBack permissionCallBack) { - if (mListPermissions == null) { - mListPermissions = new ArrayList<>(); - addAllPermissions(mListPermissions); - } - if (!isOverMarshmallow()) { - permissionCallBack.onPermissionSuccess(); - return; - } - mListPermissions = findDeniedPermissions(activity, mListPermissions); - if (mListPermissions != null && mListPermissions.size() > 0) { - activity.requestPermissions(mListPermissions.toArray(new String[mListPermissions.size()]), - requestCode); - } else { - permissionCallBack.onPermissionSuccess(); - } - } - - public void requestResult(Activity activity, String[] permissions, int[] grantResults, PermissionCallBack permissionCallBack) { - mListPermissions = new ArrayList<>(); - ArrayList noPermissions = new ArrayList<>(); - ArrayList rejectPermissons = new ArrayList<>(); - for (int i = 0; i < grantResults.length; i++) { - if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { - if (isOverMarshmallow()) { - boolean isShould = activity.shouldShowRequestPermissionRationale(permissions[i]); - mListPermissions.add(permissions[i]); - if (isShould) { - noPermissions.add(permissions[i]); - } else { - rejectPermissons.add(permissions[i]); - } - } - } - } - - if (noPermissions.size() > 0) { - permissionCallBack.onPermissionFail(); - } else if (rejectPermissons.size() > 0) { - ArrayList permissonsList = new ArrayList<>(); - for (int i = 0; i < rejectPermissons.size(); i++) { - String strPermissons = rejectPermissons.get(i); - if (PERMISSIONS_STORAGE.equals(strPermissons)) { - permissonsList.add("storage"); - } else if (PERMISSIONS_PHONE.equals(strPermissons)) { - permissonsList.add("phone"); - } - } - String strPermissons = permissonsList.toString(); - strPermissons = strPermissons.replace("[", ""); - strPermissons = strPermissons.replace("]", ""); - strPermissons = strPermissons.replace(",", "、"); - String strAppName = activity.getString(R.string.app_name); - String strMessage = "permission"; - strMessage = String.format(strMessage, strAppName, strPermissons, "\""); - permissionCallBack.onPermissionReject(strMessage); - - } else { - permissionCallBack.onPermissionSuccess(); - } - } - - public interface PermissionCallBack { - void onPermissionSuccess(); - - void onPermissionReject(String strMessage); - - void onPermissionFail(); - } -} diff --git a/app/src/main/java/com/novel/read/utlis/PhoneUtils.kt b/app/src/main/java/com/novel/read/utlis/PhoneUtils.kt deleted file mode 100644 index d4f635e..0000000 --- a/app/src/main/java/com/novel/read/utlis/PhoneUtils.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.novel.read.utlis - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Build -import android.telephony.TelephonyManager -import java.util.* - -/** - * 获取手机国际识别码IMEI - */ -class PhoneUtils(private val ctx: Context) { - - private val telephonemanager: TelephonyManager = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - private var IMSI: String? = null - - /** - * 获取手机号码 - */ - val nativePhoneNumber: String? - @SuppressLint("MissingPermission") - get() { - - var nativephonenumber: String? = null - nativephonenumber = telephonemanager.line1Number - - return nativephonenumber - } - - /** - * 获取手机服务商信息 - */ - //IMSI前面三位460是国家号码,其次的两位是运营商代号,00、02是中国移动,01是联通,03是电信。 - val providersName: String? - @SuppressLint("MissingPermission") - get() { - var providerName: String? = null - try { - IMSI = telephonemanager.subscriberId - print("IMSI是:" + IMSI!!) - if (IMSI!!.startsWith("46000") || IMSI!!.startsWith("46002")) { - providerName = "中国移动" - } else if (IMSI!!.startsWith("46001")) { - providerName = "中国联通" - } else if (IMSI!!.startsWith("46003")) { - providerName = "中国电信" - } - - } catch (e: Exception) { - e.printStackTrace() - } - - return providerName - - } - - /** - * 获取手机信息 - */ - // return ("\nDeviceID(IMEI)" + tm.getDeviceId()) + - // "\nDeviceSoftwareVersion:" + tm.getDeviceSoftwareVersion() + - // "\ngetLine1Number:" + tm.getLine1Number() + - // "\nNetworkCountryIso:" + tm.getNetworkCountryIso() + - // "\nNetworkOperator:" + tm.getNetworkOperator() + - // "\nNetworkOperatorName:" + tm.getNetworkOperatorName() + - // "\nNetworkType:" + tm.getNetworkType() + - // "\nPhoneType:" + tm.getPhoneType() + - // "\nSimCountryIso:" + tm.getSimCountryIso() + - // "\nSimOperator:" + tm.getSimOperator() + - // "\nSimOperatorName:" + tm.getSimOperatorName() + - // "\nSimSerialNumber:" + tm.getSimSerialNumber() + - // "\ngetSimState:" + tm.getSimState() + - // "\nSubscriberId:" + tm.getSubscriberId() + - // "\nVoiceMailNumber:" + tm.getVoiceMailNumber(); - val phoneInfo: String - @SuppressLint("MissingPermission") - get() { - - val tm = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - return tm.deviceId - - } - - companion object { - - // 35是IMEI开头的号 - //13 位 - //API>=9 使用serial号 - //serial需要一个初始化 - // 随便一个初始化 - //使用硬件信息拼凑出来的15位号码 - val uniquePsuedoID: String - get() { - var serial: String? = null - val m_szDevIDShort = ("35" + - Build.BOARD.length % 10 + Build.BRAND.length % 10 - + Build.CPU_ABI.length % 10 - + Build.DEVICE.length % 10 - + Build.DISPLAY.length % 10 - + Build.HOST.length % 10 - + Build.ID.length % 10 - + Build.MANUFACTURER.length % 10 - + Build.MODEL.length % 10 + Build.PRODUCT.length % 10 - + Build.TAGS.length % 10 + Build.TYPE.length % 10 + - Build.USER.length % 10) - try { - serial = Build::class.java.getField("SERIAL").get(null).toString() - return UUID( - m_szDevIDShort.hashCode().toLong(), - serial.hashCode().toLong() - ).toString() - } catch (exception: Exception) { - serial = "serial" - } - - return UUID( - m_szDevIDShort.hashCode().toLong(), - serial!!.hashCode().toLong() - ).toString() - } - } -} diff --git a/app/src/main/java/com/novel/read/utlis/RxUtils.java b/app/src/main/java/com/novel/read/utlis/RxUtils.java deleted file mode 100644 index d4f08c6..0000000 --- a/app/src/main/java/com/novel/read/utlis/RxUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.novel.read.utlis; - - - -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.Single; -import io.reactivex.SingleSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - -public class RxUtils { - - public static SingleSource toSimpleSingle(Single upstream){ - return upstream.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - - public static ObservableSource toSimpleSingle(Observable upstream){ - return upstream.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - - public static TwoTuple twoTuple(T first,R second){ - return new TwoTuple(first, second); - } - - - public static class TwoTuple { - public final A first; - public final B second; - - public TwoTuple(A a, B b) { - this.first = a; - this.second = b; - } - } -} diff --git a/app/src/main/java/com/novel/read/utlis/ScreenUtils.kt b/app/src/main/java/com/novel/read/utlis/ScreenUtils.kt deleted file mode 100644 index 126dbcd..0000000 --- a/app/src/main/java/com/novel/read/utlis/ScreenUtils.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.novel.read.utlis - -import android.util.DisplayMetrics -import android.util.TypedValue -import androidx.appcompat.app.AppCompatActivity -import com.novel.read.base.MyApp - -object ScreenUtils { - - /** - * 获取手机显示App区域的大小(头部导航栏+ActionBar+根布局),不包括虚拟按钮 - * @return - */ - val appSize: IntArray - get() { - val size = IntArray(2) - val metrics = displayMetrics - size[0] = metrics.widthPixels - size[1] = metrics.heightPixels - return size - } - - /** - * 获取导航栏的高度 - * @return - */ - val statusBarHeight: Int - get() { - val resources = MyApp.context.resources - val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") - return resources.getDimensionPixelSize(resourceId) - } - - /** - * 获取虚拟按键的高度 - * @return - */ - val navigationBarHeight: Int - get() { - var navigationBarHeight = 0 - val rs = MyApp.context.resources - val id = rs.getIdentifier("navigation_bar_height", "dimen", "android") - if (id > 0 && hasNavigationBar()) { - navigationBarHeight = rs.getDimensionPixelSize(id) - } - return navigationBarHeight - } - - private val displayMetrics: DisplayMetrics - get() = MyApp.context - .resources - .displayMetrics - - fun dpToPx(dp: Int): Int { - val metrics = displayMetrics - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), metrics).toInt() - } - - fun pxToDp(px: Int): Int { - val metrics = displayMetrics - return (px / metrics.density).toInt() - } - - fun spToPx(sp: Int): Int { - val metrics = displayMetrics - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp.toFloat(), metrics).toInt() - } - - fun pxToSp(px: Int): Int { - val metrics = displayMetrics - return (px / metrics.scaledDensity).toInt() - } - - /** - * 获取整个手机屏幕的大小(包括虚拟按钮) - * 必须在onWindowFocus方法之后使用 - * @param activity - * @return - */ - fun getScreenSize(activity: AppCompatActivity): IntArray { - val size = IntArray(2) - val decorView = activity.window.decorView - size[0] = decorView.width - size[1] = decorView.height - return size - } - - /** - * 是否存在虚拟按键 - * @return - */ - private fun hasNavigationBar(): Boolean { - var hasNavigationBar = false - val rs = MyApp.context.resources - val id = rs.getIdentifier("config_showNavigationBar", "bool", "android") - if (id > 0) { - hasNavigationBar = rs.getBoolean(id) - } - try { - val systemPropertiesClass = Class.forName("android.os.SystemProperties") - val m = systemPropertiesClass.getMethod("get", String::class.java) - val navBarOverride = m.invoke(systemPropertiesClass, "qemu.hw.mainkeys") as String - if ("1" == navBarOverride) { - hasNavigationBar = false - } else if ("0" == navBarOverride) { - hasNavigationBar = true - } - } catch (e: Exception) { - } - - return hasNavigationBar - } -} diff --git a/app/src/main/java/com/novel/read/utlis/SpUtil.java b/app/src/main/java/com/novel/read/utlis/SpUtil.java deleted file mode 100644 index f93ea5f..0000000 --- a/app/src/main/java/com/novel/read/utlis/SpUtil.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.novel.read.utlis; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; - -import java.util.Locale; - -public class SpUtil { - - private static SharedPreferences sp; - private static SpUtil sInstance; - - public static void init(Context context) { - sp = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); - } - - public static SpUtil getInstance(){ - if(sInstance == null){ - synchronized (SpUtil.class){ - if (sInstance == null){ - sInstance = new SpUtil(); - } - } - } - return sInstance; - } - - public static String getStringValue(String key) { - return sp.getString(key, null); - } - - public static String getStringValue(String key, String defaultValue) { - return sp.getString(key, defaultValue); - } - - public static void setStringValue(String key, String value) { - Editor editor = sp.edit(); - editor.putString(key, value); - editor.apply(); - } - - public static boolean getBooleanValue(String key) { - return sp.getBoolean(key, false); - } - - public static boolean getBooleanValue(String key, boolean value) { - return sp.getBoolean(key, value); - } - - public static void setBooleanValue(String key, boolean value) { - Editor editor = sp.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - public static int getIntValue(String key,int def) { - return sp.getInt(key, def); - } - - public static void setLongValue(String key, long value) { - Editor editor = sp.edit(); - editor.putLong(key, value); - editor.apply(); - } - - public static long getLongValue(String key) { - return sp.getLong(key, 0); - } - - public static void setIntValue(String key, int value) { - Editor editor = sp.edit(); - editor.putInt(key, value); - editor.apply(); - } - - public static float getFloatValue(String key) { - return sp.getFloat(key, 0.0f); - } - - public static void setFloatValue(String key, float value) { - Editor editor = sp.edit(); - editor.putFloat(key, value); - editor.apply(); - } - - public static void clearAllValue(Context context) { - SharedPreferences sharedData = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); - Editor editor = sharedData.edit(); - editor.clear(); - editor.apply(); - } - - private final String TAG_LANGUAGE = "language_select"; - private Locale systemCurrentLocal = Locale.ENGLISH; - - - public void saveLanguage(int select) { - setIntValue(TAG_LANGUAGE, select); - } - - public int getSelectLanguage() { - return getIntValue(TAG_LANGUAGE, 0); - } - - public Locale getSystemCurrentLocal() { - return systemCurrentLocal; - } - - public void setSystemCurrentLocal(Locale local) { - systemCurrentLocal = local; - } - - -} diff --git a/app/src/main/java/com/novel/read/utlis/StatusBarUtil.kt b/app/src/main/java/com/novel/read/utlis/StatusBarUtil.kt deleted file mode 100644 index 1a49afa..0000000 --- a/app/src/main/java/com/novel/read/utlis/StatusBarUtil.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.novel.read.utlis - -import android.os.Build -import android.view.View -import android.view.WindowManager - -import androidx.appcompat.app.AppCompatActivity - -import java.lang.reflect.Field -import java.lang.reflect.Method - -/** - * @author: LiJun 390057892@qq.com - * @date: 2018/3/7 15:07 - */ - -object StatusBarUtil { - fun setBarsStyle(activity: AppCompatActivity, color: Int, dark: Boolean) { - MIUISetStatusBarLightMode(activity, dark) - FlymeSetStatusBarLightMode(activity, dark) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0及以上 - val decorView = activity.window.decorView - if (dark) { - val option = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - decorView.systemUiVisibility = option - activity.window.statusBarColor = activity.resources.getColor(color) - } else { - val option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - decorView.systemUiVisibility = option - activity.window.statusBarColor = activity.resources.getColor(color) - } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {//4.4到5.0 - val localLayoutParams = activity.window.attributes - localLayoutParams.flags = - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or localLayoutParams.flags - } - - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//android6.0以后可以对状态栏文字颜色和图标进行修改 - // if (dark){ - // activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - // }else { - // activity.getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - // } - // - // } - } - - fun MIUISetStatusBarLightMode(activity: AppCompatActivity, dark: Boolean): Boolean { - var result = false - if (activity.window != null) { - val clazz = activity.window.javaClass - try { - var darkModeFlag = 0 - val layoutParams = Class.forName("android.view.MiuiWindowManager\$LayoutParams") - val field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE") - darkModeFlag = field.getInt(layoutParams) - val extraFlagField = clazz.getMethod( - "setExtraFlags", - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType - ) - if (dark) { - extraFlagField.invoke(activity.window, darkModeFlag, darkModeFlag)//状态栏透明且黑色字体 - } else { - extraFlagField.invoke(activity.window, 0, darkModeFlag)//清除黑色字体 - } - result = true - } catch (e: Exception) { - - } - - } - return result - } - - private fun FlymeSetStatusBarLightMode(activity: AppCompatActivity, dark: Boolean): Boolean { - var result = false - if (activity.window != null) { - try { - val lp = activity.window.attributes - val darkFlag = WindowManager.LayoutParams::class.java - .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON") - val meizuFlags = WindowManager.LayoutParams::class.java - .getDeclaredField("meizuFlags") - darkFlag.isAccessible = true - meizuFlags.isAccessible = true - val bit = darkFlag.getInt(null) - var value = meizuFlags.getInt(lp) - if (dark) { - value = value or bit - } else { - value = value and bit.inv() - } - meizuFlags.setInt(lp, value) - activity.window.attributes = lp - result = true - } catch (e: Exception) { - - } - - } - return result - } - - -} diff --git a/app/src/main/java/com/novel/read/utlis/StringUtils.kt b/app/src/main/java/com/novel/read/utlis/StringUtils.kt deleted file mode 100644 index 82cc46e..0000000 --- a/app/src/main/java/com/novel/read/utlis/StringUtils.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.novel.read.utlis - -import android.text.TextUtils -import com.novel.read.widget.page.ReadSettingManager.Companion.SHARED_READ_CONVERT_TYPE - -import com.spreada.utils.chinese.ZHConverter - -object StringUtils { - - /** - * 将文本中的半角字符,转换成全角字符 - */ - fun halfToFull(input: String): String { - val text = deleteImgs(input) - val c = text.toCharArray() - for (i in c.indices) { - if (c[i].toInt() == 32) - //半角空格 - { - c[i] = 12288.toChar() - continue - } - //根据实际情况,过滤不需要转换的符号 - //if (c[i] == 46) //半角点号,不转换 - // continue; - - if (c[i].toInt() in 33..126) - //其他符号都转换为全角 - c[i] = (c[i].toInt() + 65248).toChar() - } - return String(c) - } - - private fun deleteImgs(content: String?): String { - return if (content != null && !TextUtils.isEmpty(content)) { - // 去掉所有html元素, - var str = - content.replace("&[a-zA-Z]{1,10};".toRegex(), "").replace("<[^>]*>".toRegex(), "") - str = str.replace("[(/>)<]".toRegex(), "") - str - } else { - "" - } - } - - fun delete160(des: String): String { - var text = des - text = text.replace(" ".toRegex(), "") - text = text.replace("&#160;".toRegex(), "") - text = text.replace("\\s*".toRegex(), "") - text = text.trim { it <= ' ' } - return text - } - - //繁簡轉換 - fun convertCC(input: String): String { - val convertType = SpUtil.getIntValue(SHARED_READ_CONVERT_TYPE, 0) - - if (input.isEmpty()) - return "" - - return if (convertType != 0) ZHConverter.getInstance(ZHConverter.TRADITIONAL).convert(input) else input - } - -} diff --git a/app/src/main/java/com/novel/read/utlis/SystemBarUtils.kt b/app/src/main/java/com/novel/read/utlis/SystemBarUtils.kt deleted file mode 100644 index 751efb5..0000000 --- a/app/src/main/java/com/novel/read/utlis/SystemBarUtils.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.novel.read.utlis - -import android.app.Activity -import android.os.Build -import android.view.View -import android.view.WindowManager - -/** - * Created by newbiechen on 17-5-16. - * 基于 Android 4.4 - * - * 主要参数说明: - * - * SYSTEM_UI_FLAG_FULLSCREEN : 隐藏StatusBar - * SYSTEM_UI_FLAG_HIDE_NAVIGATION : 隐藏NavigationBar - * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN: 视图扩展到StatusBar的位置,并且StatusBar不消失。 - * 这里需要一些处理,一般是将StatusBar设置为全透明或者半透明。之后还需要使用fitSystemWindows=防止视图扩展到Status - * Bar上面(会在StatusBar上加一层View,该View可被移动) - * SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION: 视图扩展到NavigationBar的位置 - * SYSTEM_UI_FLAG_LAYOUT_STABLE:稳定效果 - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY:保证点击任意位置不会退出 - * - * 可设置特效说明: - * 1. 全屏特效 - * 2. 全屏点击不退出特效 - * 3. 注意在19 <=sdk <=21 时候,必须通过Window设置透明栏 - */ - -object SystemBarUtils { - - private val UNSTABLE_STATUS = View.SYSTEM_UI_FLAG_FULLSCREEN - private val UNSTABLE_NAV = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - private val STABLE_STATUS = View.SYSTEM_UI_FLAG_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - private val STABLE_NAV = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - private val EXPAND_STATUS = - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - private val EXPAND_NAV = - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - - - //设置隐藏StatusBar(点击任意地方会恢复) - fun hideUnStableStatusBar(activity: Activity) { - //App全屏,隐藏StatusBar - setFlag(activity, UNSTABLE_STATUS) - } - - fun showUnStableStatusBar(activity: Activity) { - clearFlag(activity, UNSTABLE_STATUS) - } - - //隐藏NavigationBar(点击任意地方会恢复) - fun hideUnStableNavBar(activity: Activity) { - setFlag(activity, UNSTABLE_NAV) - } - - fun showUnStableNavBar(activity: Activity) { - clearFlag(activity, UNSTABLE_NAV) - } - - fun hideStableStatusBar(activity: Activity) { - //App全屏,隐藏StatusBar - setFlag(activity, STABLE_STATUS) - } - - fun showStableStatusBar(activity: Activity) { - clearFlag(activity, STABLE_STATUS) - } - - fun hideStableNavBar(activity: Activity) { - //App全屏,隐藏StatusBar - setFlag(activity, STABLE_NAV) - } - - fun showStableNavBar(activity: Activity) { - clearFlag(activity, STABLE_NAV) - } - - /** - * 视图扩充到StatusBar - */ - fun expandStatusBar(activity: Activity) { - setFlag(activity, EXPAND_STATUS) - } - - /** - * 视图扩充到NavBar - * @param activity - */ - fun expandNavBar(activity: Activity) { - setFlag(activity, EXPAND_NAV) - } - - fun transparentStatusBar(activity: Activity) { - if (Build.VERSION.SDK_INT >= 21) { - expandStatusBar(activity) - activity.window.statusBarColor = - activity.resources.getColor(android.R.color.transparent) - } else if (Build.VERSION.SDK_INT >= 19) { - val attrs = activity.window.attributes - attrs.flags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or attrs.flags - activity.window.attributes = attrs - } - } - - fun transparentNavBar(activity: Activity) { - if (Build.VERSION.SDK_INT >= 21) { - expandNavBar(activity) - //下面这个方法在sdk:21以上才有 - activity.window.navigationBarColor = - activity.resources.getColor(android.R.color.transparent) - } - } - - fun setFlag(activity: Activity, flag: Int) { - if (Build.VERSION.SDK_INT >= 19) { - val decorView = activity.window.decorView - val option = decorView.systemUiVisibility or flag - decorView.systemUiVisibility = option - } - } - - //取消flag - fun clearFlag(activity: Activity, flag: Int) { - if (Build.VERSION.SDK_INT >= 19) { - val decorView = activity.window.decorView - val option = decorView.systemUiVisibility and flag.inv() - decorView.systemUiVisibility = option - } - } - - fun setToggleFlag(activity: Activity, option: Int) { - if (Build.VERSION.SDK_INT >= 19) { - if (isFlagUsed(activity, option)) { - clearFlag(activity, option) - } else { - setFlag(activity, option) - } - } - } - - /** - * @param activity - * @return flag是否已被使用 - */ - fun isFlagUsed(activity: Activity, flag: Int): Boolean { - val currentFlag = activity.window.decorView.systemUiVisibility - return currentFlag and flag == flag - } -} diff --git a/app/src/main/java/com/novel/read/utlis/VersionUtil.kt b/app/src/main/java/com/novel/read/utlis/VersionUtil.kt deleted file mode 100644 index a88c30b..0000000 --- a/app/src/main/java/com/novel/read/utlis/VersionUtil.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.novel.read.utlis - -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager - -object VersionUtil { - - fun getPackageCode(context: Context): Int { - val manager = context.packageManager - var code = 0 - try { - val info = manager.getPackageInfo(context.packageName, 0) - code = info.versionCode - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - } - - return code - } - - fun getPackageName(context: Context): String? { - val manager = context.packageManager - var name: String? = null - try { - val info = manager.getPackageInfo(context.packageName, 0) - name = info.versionName - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - } - - return name - } - - /** - * [获取应用程序版本名称信息] - * @param context - * @return 当前应用的版本名称 - */ - fun getPackage(context: Context): String? { - try { - val packageManager = context.packageManager - val packageInfo = packageManager.getPackageInfo( - context.packageName, 0 - ) - return packageInfo.packageName - } catch (e: Exception) { - e.printStackTrace() - } - - return null - } - - -} diff --git a/app/src/main/java/com/novel/read/widget/HeadLayout.kt b/app/src/main/java/com/novel/read/widget/HeadLayout.kt deleted file mode 100644 index c99cc35..0000000 --- a/app/src/main/java/com/novel/read/widget/HeadLayout.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.novel.read.widget - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.LinearLayout -import androidx.core.content.ContextCompat -import com.novel.read.R -import kotlinx.android.synthetic.main.widget_head.view.* - -class HeadLayout(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { - - init { - LayoutInflater.from(context).inflate(R.layout.widget_head, this, true) - initAttr(attrs) - } - - private fun initAttr(attrs: AttributeSet?) { - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.HeadLayout) - val showHead = typedArray.getBoolean(R.styleable.HeadLayout_showHead, true) - val headText = typedArray.getResourceId(R.styleable.HeadLayout_headText, R.string.empty_info) - val headImg = typedArray.getResourceId(R.styleable.HeadLayout_headImg, R.mipmap.ic_launcher) - val showRight = typedArray.getBoolean(R.styleable.HeadLayout_showRightText, true) - val rightText = typedArray.getResourceId(R.styleable.HeadLayout_rightText, R.string.empty_info) - val rightImg = typedArray.getResourceId(R.styleable.HeadLayout_rightImg, R.mipmap.ic_launcher) - if (showHead) { - tv_head.setText(headText) - val drawable = ContextCompat.getDrawable(context, headImg) - drawable!!.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) - tv_head.setCompoundDrawables(drawable, null, null, null) - } else { - tv_head.visibility = View.GONE - } - if (showRight) { - tv_more.setText(rightText) - if (rightImg != R.mipmap.ic_launcher) { - val drawable = ContextCompat.getDrawable(context, rightImg) - drawable!!.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) - tv_head.setCompoundDrawables(null, null, drawable, null) - } - } else { - tv_more.visibility = View.GONE - } - - typedArray.recycle() - } - - //设置点击右边文字的监听 - fun setRightTextClickListener(listener: View.OnClickListener) { - tv_more.setOnClickListener(listener) - } - - -} diff --git a/app/src/main/java/com/novel/read/widget/VpTabLayout.kt b/app/src/main/java/com/novel/read/widget/VpTabLayout.kt deleted file mode 100644 index 84c04fc..0000000 --- a/app/src/main/java/com/novel/read/widget/VpTabLayout.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.novel.read.widget - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator -import android.content.Context -import android.content.res.TypedArray -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.animation.AccelerateInterpolator -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.viewpager.widget.ViewPager - -import com.novel.read.R -import kotlinx.android.synthetic.main.widget_tab.view.* - -class VpTabLayout(private val mContext: Context, attrs: AttributeSet) : - ConstraintLayout(mContext, attrs), View.OnClickListener { - - private var animFinish = true //防止动画还未结束就开始另一个动画 - private var mBtnClickListener: OnTabClickListener? = null - - init { - LayoutInflater.from(mContext).inflate(R.layout.widget_tab, this, true) - initViews() - initAttr(attrs) - } - - private fun initViews() { - tv_one.setOnClickListener(this) - tv_second.setOnClickListener(this) - tv_third.setOnClickListener(this) - } - - - private fun initAttr(attrs: AttributeSet) { - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.VpTabLayout) - val oneText = typedArray.getResourceId(R.styleable.VpTabLayout_oneText, R.string.empty_info) - val twoText = typedArray.getResourceId(R.styleable.VpTabLayout_twoText, R.string.empty_info) - val threeText = - typedArray.getResourceId(R.styleable.VpTabLayout_threeText, R.string.empty_info) - - tv_one.setText(oneText) - tv_second.setText(twoText) - tv_third.setText(threeText) - - typedArray.recycle() - } - - - //设置动画 - fun setAnim(position: Int, viewPager: ViewPager) { - if (!animFinish) { - return - } - animFinish = false - var animator: ValueAnimator? = null - when (position) { - 0 -> { - animator = ValueAnimator.ofFloat(view.x, tv_one.x) - animator!!.setTarget(tv_one) - animator.addUpdateListener { valueAnimator -> - view.x = valueAnimator.animatedValue as Float - } - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - viewPager.currentItem = position - tv_one.setTextColor(ContextCompat.getColor(mContext, R.color.white)) - tv_second.setTextColor(ContextCompat.getColor(mContext, R.color.colorTitle)) - tv_third.setTextColor( - ContextCompat.getColor( - mContext, - R.color.colorTitle - ) - ) - animFinish = true - } - }) - } - 1 -> { - animator = ValueAnimator.ofFloat(view.x, tv_second.x) - animator!!.setTarget(tv_second) - animator.addUpdateListener { valueAnimator -> - view.x = valueAnimator.animatedValue as Float - } - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - viewPager.currentItem = position - tv_one.setTextColor(ContextCompat.getColor(mContext, R.color.colorTitle)) - tv_second.setTextColor(ContextCompat.getColor(mContext, R.color.white)) - tv_third.setTextColor( - ContextCompat.getColor( - mContext, - R.color.colorTitle - ) - ) - animFinish = true - } - }) - } - 2 -> { - animator = ValueAnimator.ofFloat(view.x, tv_third.x) - animator!!.setTarget(tv_third) - animator.addUpdateListener { valueAnimator -> - view.x = valueAnimator.animatedValue as Float - } - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - viewPager.currentItem = position - tv_one.setTextColor(ContextCompat.getColor(mContext, R.color.colorTitle)) - tv_second.setTextColor(ContextCompat.getColor(mContext, R.color.colorTitle)) - tv_third.setTextColor(ContextCompat.getColor(mContext, R.color.white)) - animFinish = true - } - }) - } - else -> { - } - } - if (animator != null) { - animator.duration = 200 - animator.interpolator = AccelerateInterpolator() - animator.start() - } - } - - fun setOnTabBtnClickListener(listener: OnTabClickListener?) { - this.mBtnClickListener = listener - } - - override fun onClick(v: View) { - if (this.mBtnClickListener != null) { - when (v) { - this.tv_one -> this.mBtnClickListener!!.onTabBtnClick(CommonTabBtn.ONE, v) - this.tv_second -> this.mBtnClickListener!!.onTabBtnClick(CommonTabBtn.TWO, v) - this.tv_third -> this.mBtnClickListener!!.onTabBtnClick(CommonTabBtn.THREE, v) - } - } - - } - - enum class CommonTabBtn() { - ONE, - TWO, - THREE - } - - interface OnTabClickListener { - fun onTabBtnClick(var1: CommonTabBtn, var2: View) - } -} diff --git a/app/src/main/java/com/novel/read/widget/dialog/ReadSettingDialog.kt b/app/src/main/java/com/novel/read/widget/dialog/ReadSettingDialog.kt deleted file mode 100644 index 65f94f6..0000000 --- a/app/src/main/java/com/novel/read/widget/dialog/ReadSettingDialog.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.novel.read.widget.dialog - -import android.app.Activity -import android.app.Dialog -import android.graphics.drawable.Drawable -import android.view.Gravity -import android.view.View -import android.view.WindowManager -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.GridLayoutManager -import com.novel.read.R -import com.novel.read.adapter.PageStyleAdapter -import com.novel.read.utlis.ScreenUtils -import com.novel.read.widget.page.PageLoader -import com.novel.read.widget.page.PageMode -import com.novel.read.widget.page.PageStyle -import com.novel.read.widget.page.ReadSettingManager -import kotlinx.android.synthetic.main.layout_setting.* - -class ReadSettingDialog(mActivity: Activity, private var mPageLoader: PageLoader) : - Dialog(mActivity, R.style.ReadSettingDialog) { - - private lateinit var mPageStyleAdapter: PageStyleAdapter - private var mSettingManager: ReadSettingManager = ReadSettingManager.getInstance() - - private var mPageMode: PageMode? = null - private var mPageStyle: PageStyle? = null - - private var mBrightness: Int = 0 - private var mTextSize: Int = 0 - - private var isBrightnessAuto: Boolean = false - private var isTextDefault: Boolean = false - - private var convertType: Int = 0 - - init { - setContentView(R.layout.layout_setting) - setUpWindow() - initData() - initWidget() - initClick() - initPageMode() - } - - //设置Dialog显示的位置 - private fun setUpWindow() { - val window = window - val lp = window!!.attributes - lp.width = WindowManager.LayoutParams.MATCH_PARENT - lp.height = WindowManager.LayoutParams.WRAP_CONTENT - lp.gravity = Gravity.BOTTOM - window.attributes = lp - } - - private fun initData() { - - isBrightnessAuto = mSettingManager.isBrightnessAuto - mBrightness = mSettingManager.brightness - mTextSize = mSettingManager.textSize - isTextDefault = mSettingManager.isDefaultTextSize - mPageMode = mSettingManager.pageMode - mPageStyle = mSettingManager.pageStyle - convertType = mSettingManager.convertType - if (convertType == 0) { - tv_simple.isSelected = true - tv_trans.isSelected = false - } else { - tv_simple.isSelected = false - tv_trans.isSelected = true - } - tv_size.text = "$mTextSize" - } - - private fun initWidget() { - setUpAdapter() - } - - private fun setUpAdapter() { - val drawables = arrayOf( - getDrawable(R.color.read_bg_one), - getDrawable(R.color.read_bg_two), - getDrawable(R.color.read_bg_four), - getDrawable(R.color.read_bg_five) - ) - - mPageStyleAdapter = PageStyleAdapter(listOf(*drawables) as List, mPageLoader) - read_setting_rv_bg.layoutManager = GridLayoutManager(context, 4) - read_setting_rv_bg.adapter = mPageStyleAdapter - - mPageStyleAdapter.setPageStyleChecked(mPageStyle!!) - - } - - private fun getDrawable(drawRes: Int): Drawable? { - return ContextCompat.getDrawable(context, drawRes) - } - - private fun initClick() { - - //字体大小调节 - read_setting_tv_font_minus.setOnClickListener { - val fontSize = mSettingManager.textSize - 1 - if (fontSize < 0) { - return@setOnClickListener - } - mPageLoader.setTextSize(fontSize) - tv_size.text = "$fontSize" - } - - read_setting_tv_font_plus.setOnClickListener { - val fontSize = mSettingManager.textSize + 1 - mPageLoader.setTextSize(fontSize) - tv_size.text = "$fontSize" - } - - tv_font_default.setOnClickListener { - val fontSize = ScreenUtils.spToPx(16) - mSettingManager.textSize = fontSize - mPageLoader.setTextSize(fontSize) - tv_size.text = "$fontSize" - } - - tv_simple.setOnClickListener(View.OnClickListener { - if (convertType == 0) { - return@OnClickListener - } - tv_simple.isSelected = true - tv_trans.isSelected = false - mSettingManager.convertType = 0 - convertType = 0 - mPageLoader.setTextSize(mSettingManager.textSize) - }) - - tv_trans.setOnClickListener(View.OnClickListener { - if (convertType == 1) { - return@OnClickListener - } - tv_simple.isSelected = false - tv_trans.isSelected = true - mSettingManager.convertType = 1 - convertType = 1 - mPageLoader.setTextSize(mSettingManager.textSize) - }) - - //Page Mode 切换 - read_setting_rg_page_mode.setOnCheckedChangeListener { group, checkedId -> - val pageMode: PageMode = when (checkedId) { - R.id.read_setting_rb_simulation -> PageMode.SIMULATION - R.id.read_setting_rb_cover -> PageMode.COVER - R.id.read_setting_rb_scroll -> PageMode.SCROLL - R.id.read_setting_rb_none -> PageMode.NONE - else -> PageMode.SIMULATION - } - mPageLoader.setPageMode(pageMode) - } - - } - - private fun initPageMode() { - when (mPageMode) { - PageMode.SIMULATION -> read_setting_rb_simulation.isChecked = true - PageMode.COVER -> read_setting_rb_cover.isChecked = true - PageMode.NONE -> read_setting_rb_none.isChecked = true - PageMode. SCROLL -> read_setting_rb_scroll.isChecked = true - else -> { - - } - } - } - - companion object { - private val TAG = "ReadSettingDialog" - } - -} diff --git a/app/src/main/java/com/novel/read/widget/page/LocalPageLoader.java b/app/src/main/java/com/novel/read/widget/page/LocalPageLoader.java deleted file mode 100644 index 6c0891c..0000000 --- a/app/src/main/java/com/novel/read/widget/page/LocalPageLoader.java +++ /dev/null @@ -1,442 +0,0 @@ -package com.novel.read.widget.page; - -import android.content.Context; -import android.util.Log; - -import com.novel.read.constants.Constant; -import com.novel.read.model.db.BookChapterBean; -import com.novel.read.model.db.CollBookBean; -import com.novel.read.model.db.dbManage.BookRepository; -import com.novel.read.utlis.Charset; -import com.novel.read.utlis.DateUtli; -import com.novel.read.utlis.FileUtils; -import com.novel.read.utlis.IOUtils; -import com.novel.read.utlis.MD5Utils; -import com.novel.read.utlis.RxUtils; -import com.novel.read.utlis.StringUtils; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.RandomAccessFile; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.reactivex.Single; -import io.reactivex.SingleEmitter; -import io.reactivex.SingleObserver; -import io.reactivex.SingleOnSubscribe; -import io.reactivex.disposables.Disposable; - -/** - * Created by zlj - */ -public class LocalPageLoader extends PageLoader { - private static final String TAG = "LocalPageLoader"; - //默认从文件中获取数据的长度 - private final static int BUFFER_SIZE = 512 * 1024; - //没有标题的时候,每个章节的最大长度 - private final static int MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024; - - // "序(章)|前言" - private final static Pattern mPreChapterPattern = Pattern.compile("^(\\s{0,10})((\u5e8f[\u7ae0\u8a00]?)|(\u524d\u8a00)|(\u6954\u5b50))(\\s{0,10})$", Pattern.MULTILINE); - - //正则表达式章节匹配模式 - // "(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节回集卷])(.*)" - private static final String[] CHAPTER_PATTERNS = new String[]{"^(.{0,8})(\u7b2c)([0-9\u96f6\u4e00\u4e8c\u4e24\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u62fe\u4f70\u4edf]{1,10})([\u7ae0\u8282\u56de\u96c6\u5377])(.{0,30})$", - "^(\\s{0,4})([\\(\u3010\u300a]?(\u5377)?)([0-9\u96f6\u4e00\u4e8c\u4e24\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u62fe\u4f70\u4edf]{1,10})([\\.:\uff1a\u0020\f\t])(.{0,30})$", - "^(\\s{0,4})([\\(\uff08\u3010\u300a])(.{0,30})([\\)\uff09\u3011\u300b])(\\s{0,2})$", - "^(\\s{0,4})(\u6b63\u6587)(.{0,20})$", - "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$"}; - - //章节解析模式 - private Pattern mChapterPattern = null; - //获取书本的文件 - private File mBookFile; - //编码类型 - private Charset mCharset; - - private Disposable mChapterDisp = null; - - private Context mContext; - - public LocalPageLoader(PageView pageView, CollBookBean collBook) { - super(pageView, collBook); - mStatus = STATUS_PARING; - mContext = pageView.getContext(); - } - - private List convertTxtChapter(List bookChapters) { - List txtChapters = new ArrayList<>(bookChapters.size()); - for (BookChapterBean bean : bookChapters) { - TxtChapter chapter = new TxtChapter(); - chapter.bookId = bean.getBookId(); - chapter.title = bean.getTitle(); - chapter.start = bean.getStart(); - chapter.end = bean.getEnd(); - txtChapters.add(chapter); - } - return txtChapters; - } - - /** - * 未完成的部分: - * 1. 序章的添加 - * 2. 章节存在的书本的虚拟分章效果 - * - * @throws IOException - */ - private void loadChapters() throws IOException { - List chapters = new ArrayList<>(); - //获取文件流 - RandomAccessFile bookStream = new RandomAccessFile(mBookFile, "r"); - //寻找匹配文章标题的正则表达式,判断是否存在章节名 - boolean hasChapter = checkChapterType(bookStream); - //加载章节 - byte[] buffer = new byte[BUFFER_SIZE]; - //获取到的块起始点,在文件中的位置 - long curOffset = 0; - //block的个数 - int blockPos = 0; - //读取的长度 - int length; - - //获取文件中的数据到buffer,直到没有数据为止 - while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) { - ++blockPos; - //如果存在Chapter - if (hasChapter) { - //将数据转换成String - String blockContent = new String(buffer, 0, length, mCharset.getCode()); - //当前Block下使过的String的指针 - int seekPos = 0; - //进行正则匹配 - Matcher matcher = mChapterPattern.matcher(blockContent); - //如果存在相应章节 - while (matcher.find()) { - //获取匹配到的字符在字符串中的起始位置 - int chapterStart = matcher.start(); - - //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容 - //第一种情况一定是序章 第二种情况可能是上一个章节的内容 - if (seekPos == 0 && chapterStart != 0) { - //获取当前章节的内容 - String chapterContent = blockContent.substring(seekPos, chapterStart); - //设置指针偏移 - seekPos += chapterContent.length(); - - //如果当前对整个文件的偏移位置为0的话,那么就是序章 - if (curOffset == 0) { - //创建序章 - TxtChapter preChapter = new TxtChapter(); - preChapter.title = "序章"; - preChapter.start = 0; - preChapter.end = chapterContent.getBytes(mCharset.getCode()).length; //获取String的byte值,作为最终值 - - //如果序章大小大于30才添加进去 - if (preChapter.end - preChapter.start > 30) { - chapters.add(preChapter); - } - - //创建当前章节 - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = preChapter.end; - chapters.add(curChapter); - } - //否则就block分割之后,上一个章节的剩余内容 - else { - //获取上一章节 - TxtChapter lastChapter = chapters.get(chapters.size() - 1); - //将当前段落添加上一章去 - lastChapter.end += chapterContent.getBytes(mCharset.getCode()).length; - - //如果章节内容太小,则移除 - if (lastChapter.end - lastChapter.start < 30) { - chapters.remove(lastChapter); - } - - //创建当前章节 - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = lastChapter.end; - chapters.add(curChapter); - } - } else { - //是否存在章节 - if (chapters.size() != 0) { - //获取章节内容 - String chapterContent = blockContent.substring(seekPos, matcher.start()); - seekPos += chapterContent.length(); - - //获取上一章节 - TxtChapter lastChapter = chapters.get(chapters.size() - 1); - lastChapter.end = lastChapter.start + chapterContent.getBytes(mCharset.getCode()).length; - - //如果章节内容太小,则移除 - if (lastChapter.end - lastChapter.start < 30) { - chapters.remove(lastChapter); - } - - //创建当前章节 - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = lastChapter.end; - chapters.add(curChapter); - } - //如果章节不存在则创建章节 - else { - TxtChapter curChapter = new TxtChapter(); - curChapter.title = matcher.group(); - curChapter.start = 0; - chapters.add(curChapter); - } - } - } - } - //进行本地虚拟分章 - else { - //章节在buffer的偏移量 - int chapterOffset = 0; - //当前剩余可分配的长度 - int strLength = length; - //分章的位置 - int chapterPos = 0; - - while (strLength > 0) { - ++chapterPos; - //是否长度超过一章 - if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { - //在buffer中一章的终止点 - int end = length; - //寻找换行符作为终止点 - for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) { - if (buffer[i] == Charset.Companion.getBLANK()) { - end = i; - break; - } - } - TxtChapter chapter = new TxtChapter(); - chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")"; - chapter.start = curOffset + chapterOffset + 1; - chapter.end = curOffset + end; - chapters.add(chapter); - //减去已经被分配的长度 - strLength = strLength - (end - chapterOffset); - //设置偏移的位置 - chapterOffset = end; - } else { - TxtChapter chapter = new TxtChapter(); - chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")"; - chapter.start = curOffset + chapterOffset + 1; - chapter.end = curOffset + length; - chapters.add(chapter); - strLength = 0; - } - } - } - - //block的偏移点 - curOffset += length; - - if (hasChapter) { - //设置上一章的结尾 - TxtChapter lastChapter = chapters.get(chapters.size() - 1); - lastChapter.end = curOffset; - } - - //当添加的block太多的时候,执行GC - if (blockPos % 15 == 0) { - System.gc(); - System.runFinalization(); - } - } - - mChapterList = chapters; - IOUtils.INSTANCE.close(bookStream); - - System.gc(); - System.runFinalization(); - } - - /** - * 从文件中提取一章的内容 - * - * @param chapter - * @return - */ - private byte[] getChapterContent(TxtChapter chapter) { - RandomAccessFile bookStream = null; - try { - bookStream = new RandomAccessFile(mBookFile, "r"); - bookStream.seek(chapter.start); - int extent = (int) (chapter.end - chapter.start); - byte[] content = new byte[extent]; - bookStream.read(content, 0, extent); - return content; - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - IOUtils.INSTANCE.close(bookStream); - } - - return new byte[0]; - } - - /** - * 1. 检查文件中是否存在章节名 - * 2. 判断文件中使用的章节名类型的正则表达式 - * - * @return 是否存在章节名 - */ - private boolean checkChapterType(RandomAccessFile bookStream) throws IOException { - //首先获取128k的数据 - byte[] buffer = new byte[BUFFER_SIZE / 4]; - int length = bookStream.read(buffer, 0, buffer.length); - //进行章节匹配 - for (String str : CHAPTER_PATTERNS) { - Pattern pattern = Pattern.compile(str, Pattern.MULTILINE); - Matcher matcher = pattern.matcher(new String(buffer, 0, length, mCharset.getCode())); - //如果匹配存在,那么就表示当前章节使用这种匹配方式 - if (matcher.find()) { - mChapterPattern = pattern; - //重置指针位置 - bookStream.seek(0); - return true; - } - } - - //重置指针位置 - bookStream.seek(0); - return false; - } - - @Override - public void saveRecord() { - super.saveRecord(); - //修改当前COllBook记录 - if (mCollBook != null && isChapterListPrepare) { - //表示当前CollBook已经阅读 - mCollBook.setIsUpdate(false); - mCollBook.setLastChapter(mChapterList.get(mCurChapterPos).getTitle()); - mCollBook.setLastRead(String.valueOf(System.currentTimeMillis())); - //直接更新 - BookRepository.getInstance().saveCollBook(mCollBook); - } - } - - @Override - public void closeBook() { - super.closeBook(); - if (mChapterDisp != null) { - mChapterDisp.dispose(); - mChapterDisp = null; - } - } - - @Override - public void refreshChapterList() { - // 对于文件是否存在,或者为空的判断,不作处理。 ==> 在文件打开前处理过了。 - mBookFile = new File(mCollBook.getCover()); - //获取文件编码 - mCharset = FileUtils.getCharset(mBookFile.getAbsolutePath()); - - String lastModified = DateUtli.INSTANCE.dateConvert(mBookFile.lastModified(), Constant.FORMAT_BOOK_DATE); - - // 判断文件是否已经加载过,并具有缓存 - if (!mCollBook.isUpdate() && mCollBook.getUpdated() != null - && mCollBook.getUpdated().equals(lastModified) - && mCollBook.getBookChapters() != null) { - - mChapterList = convertTxtChapter(mCollBook.getBookChapters()); - isChapterListPrepare = true; - - //提示目录加载完成 - if (mPageChangeListener != null) { - mPageChangeListener.onCategoryFinish(mChapterList); - } - - // 加载并显示当前章节 - openChapter(); - - return; - } - - // 通过RxJava异步处理分章事件 - Single.create(new SingleOnSubscribe() { - @Override - public void subscribe(SingleEmitter e) throws Exception { - loadChapters(); - e.onSuccess(new Void()); - } - }).compose(RxUtils::toSimpleSingle) - .subscribe(new SingleObserver() { - @Override - public void onSubscribe(Disposable d) { - mChapterDisp = d; - } - - @Override - public void onSuccess(Void value) { - mChapterDisp = null; - isChapterListPrepare = true; - - // 提示目录加载完成 - if (mPageChangeListener != null) { - mPageChangeListener.onCategoryFinish(mChapterList); - } - - // 存储章节到数据库 - List bookChapterBeanList = new ArrayList<>(); - for (int i = 0; i < mChapterList.size(); ++i) { - TxtChapter chapter = mChapterList.get(i); - BookChapterBean bean = new BookChapterBean(); - bean.setId(MD5Utils.INSTANCE.strToMd5By16(mBookFile.getAbsolutePath() - + File.separator + chapter.title)); // 将路径+i 作为唯一值 - bean.setTitle(chapter.getTitle()); - bean.setStart(chapter.getStart()); - bean.setUnreadble(false); - bean.setEnd(chapter.getEnd()); - bookChapterBeanList.add(bean); - } - mCollBook.setBookChapters(bookChapterBeanList); - mCollBook.setUpdated(lastModified); - - BookRepository.getInstance().saveBookChaptersWithAsync(bookChapterBeanList, mCollBook); - BookRepository.getInstance().saveCollBook(mCollBook); - - // 加载并显示当前章节 - openChapter(); - } - - @Override - public void onError(Throwable e) { - chapterError(); - Log.e(TAG, "file load error: " + e.toString()); - } - }); - } - - @Override - protected BufferedReader getChapterReader(TxtChapter chapter) throws Exception { - //从文件中获取数据 - byte[] content = getChapterContent(chapter); - String test = new String(content); - test = StringUtils.INSTANCE.convertCC(test); - content = test.getBytes(); - ByteArrayInputStream bais = new ByteArrayInputStream(content); - return new BufferedReader(new InputStreamReader(bais, mCharset.getCode())); - } - - @Override - protected boolean hasChapterData(TxtChapter chapter) { - return true; - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/NetPageLoader.java b/app/src/main/java/com/novel/read/widget/page/NetPageLoader.java deleted file mode 100644 index 625d3a1..0000000 --- a/app/src/main/java/com/novel/read/widget/page/NetPageLoader.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.novel.read.widget.page; - - -import android.content.Context; -import com.novel.read.constants.Constant; -import com.novel.read.model.db.BookChapterBean; -import com.novel.read.model.db.CollBookBean; -import com.novel.read.model.db.dbManage.BookManager; -import com.novel.read.model.db.dbManage.BookRepository; -import com.novel.read.utlis.FileUtils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.List; - -/** - * Created by zlj - * 网络页面加载器 - */ -public class NetPageLoader extends PageLoader { - private static final String TAG = "PageFactory"; - private Context mContext; - public NetPageLoader(PageView pageView, CollBookBean collBook) { - super(pageView, collBook); - mContext = pageView.getContext(); - } - - private List convertTxtChapter(List bookChapters) { - List txtChapters = new ArrayList<>(bookChapters.size()); - for (BookChapterBean bean : bookChapters) { - TxtChapter chapter = new TxtChapter(); - chapter.bookId = mCollBook.getId(); - chapter.title = bean.getTitle(); - chapter.link = bean.getLink(); - chapter.chapterId = bean.getId(); - txtChapters.add(chapter); - } - return txtChapters; - } - - @Override - public void refreshChapterList() { - if (mCollBook.getBookChapters() == null) return; - - // 将 BookChapter 转换成当前可用的 Chapter - mChapterList = convertTxtChapter(mCollBook.getBookChapters()); - isChapterListPrepare = true; - - // 目录加载完成,执行回调操作。 - if (mPageChangeListener != null) { - mPageChangeListener.onCategoryFinish(mChapterList); - } - - // 如果章节未打开 - if (!isChapterOpen()) { - // 打开章节 - openChapter(); - } - } - - @Override - protected BufferedReader getChapterReader(TxtChapter chapter) throws Exception { - File file = new File(Constant.BOOK_CACHE_PATH + mCollBook.getId() - + File.separator + chapter.title + FileUtils.SUFFIX_NB); - if (!file.exists()) return null; - Reader reader = new FileReader(file); - return new BufferedReader(reader); - } - - @Override - protected boolean hasChapterData(TxtChapter chapter) { - return BookManager.isChapterCached(mCollBook.getId(), chapter.title); - } - - // 装载上一章节的内容 - @Override - boolean parsePrevChapter() { - boolean isRight = super.parsePrevChapter(); - - if (mStatus == STATUS_FINISH) { - loadPrevChapter(); - } else if (mStatus == STATUS_LOADING) { - loadCurrentChapter(); - } - return isRight; - } - - // 装载当前章内容。 - @Override - boolean parseCurChapter() { - boolean isRight = super.parseCurChapter(); - - if (mStatus == STATUS_LOADING) { - loadCurrentChapter(); - } - return isRight; - } - - // 装载下一章节的内容 - @Override - boolean parseNextChapter() { - boolean isRight = super.parseNextChapter(); - - if (mStatus == STATUS_FINISH) { - loadNextChapter(); - } else if (mStatus == STATUS_LOADING) { - loadCurrentChapter(); - } - - return isRight; - } - - /** - * 加载当前页的前面两个章节 - */ - private void loadPrevChapter() { - if (mPageChangeListener != null) { - int end = mCurChapterPos; - int begin = end - 2; - if (begin < 0) { - begin = 0; - } - - requestChapters(begin, end); - } - } - - /** - * 加载前一页,当前页,后一页。 - */ - private void loadCurrentChapter() { - if (mPageChangeListener != null) { - int begin = mCurChapterPos; - int end = mCurChapterPos; - - // 是否当前不是最后一章 - if (end < mChapterList.size()) { - end = end + 1; - if (end >= mChapterList.size()) { - end = mChapterList.size() - 1; - } - } - - // 如果当前不是第一章 - if (begin != 0) { - begin = begin - 1; - if (begin < 0) { - begin = 0; - } - } - - requestChapters(begin, end); - } - } - - /** - * 加载当前页的后两个章节 - */ - private void loadNextChapter() { - if (mPageChangeListener != null) { - - // 提示加载后两章 - int begin = mCurChapterPos + 1; - int end = begin + 1; - - // 判断是否大于最后一章 - if (begin >= mChapterList.size()) { - // 如果下一章超出目录了,就没有必要加载了 - return; - } - - if (end > mChapterList.size()) { - end = mChapterList.size() - 1; - } - - requestChapters(begin, end); - } - } - - private void requestChapters(int start, int end) { - // 检验输入值 - if (start < 0) { - start = 0; - } - - if (end >= mChapterList.size()) { - end = mChapterList.size() - 1; - } - - - List chapters = new ArrayList<>(); - - // 过滤,哪些数据已经加载了 - for (int i = start; i <= end; ++i) { - TxtChapter txtChapter = mChapterList.get(i); - if (!hasChapterData(txtChapter)) { - chapters.add(txtChapter); - } - } - - if (!chapters.isEmpty()) { - mPageChangeListener.requestChapters(chapters); - } - } - - @Override - public void saveRecord() { - super.saveRecord(); - if (mCollBook != null && isChapterListPrepare) { - //表示当前CollBook已经阅读 - mCollBook.setIsUpdate(false); - mCollBook.setLastRead(String.valueOf(System.currentTimeMillis())); - //直接更新 - BookRepository.getInstance().saveCollBook(mCollBook); - } - } -} - diff --git a/app/src/main/java/com/novel/read/widget/page/PageAnimation.java b/app/src/main/java/com/novel/read/widget/page/PageAnimation.java deleted file mode 100644 index b0640c3..0000000 --- a/app/src/main/java/com/novel/read/widget/page/PageAnimation.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.novel.read.widget.page; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.view.MotionEvent; -import android.view.View; -import android.view.animation.LinearInterpolator; -import android.widget.Scroller; - -/** - * Created by zlj - * 翻页动画抽象类 - */ -public abstract class PageAnimation { - //正在使用的View - protected View mView; - //滑动装置 - protected Scroller mScroller; - //监听器 - protected OnPageChangeListener mListener; - //移动方向 - protected Direction mDirection = Direction.NONE; - - protected boolean isRunning = false; - - //屏幕的尺寸 - protected int mScreenWidth; - protected int mScreenHeight; - //屏幕的间距 - protected int mMarginWidth; - protected int mMarginHeight; - //视图的尺寸 - protected int mViewWidth; - protected int mViewHeight; - //起始点 - protected float mStartX; - protected float mStartY; - //触碰点 - protected float mTouchX; - protected float mTouchY; - //上一个触碰点 - protected float mLastX; - protected float mLastY; - - public PageAnimation(int w, int h, View view, OnPageChangeListener listener){ - this(w, h, 0, 0, view,listener); - } - - public PageAnimation(int w, int h, int marginWidth, int marginHeight, View view, OnPageChangeListener listener){ - mScreenWidth = w; - mScreenHeight = h; - - mMarginWidth = marginWidth; - mMarginHeight = marginHeight; - - mViewWidth = mScreenWidth - mMarginWidth * 2; - mViewHeight = mScreenHeight - mMarginHeight * 2; - - mView = view; - mListener = listener; - - mScroller = new Scroller(mView.getContext(), new LinearInterpolator()); - } - - public void setStartPoint(float x,float y){ - mStartX = x; - mStartY = y; - - mLastX = mStartX; - mLastY = mStartY; - } - - public void setTouchPoint(float x,float y){ - mLastX = mTouchX; - mLastY = mTouchY; - - mTouchX = x; - mTouchY = y; - } - - public boolean isRunning(){ - return isRunning; - } - - /** - * 开启翻页动画 - */ - public void startAnim(){ - if (isRunning){ - return; - } - isRunning = true; - } - - public void setDirection(Direction direction){ - mDirection = direction; - } - - public Direction getDirection(){ - return mDirection; - } - - public void clear(){ - mView = null; - } - /** - * 点击事件的处理 - * @param event - */ - public abstract boolean onTouchEvent(MotionEvent event); - - /** - * 绘制图形 - * @param canvas - */ - public abstract void draw(Canvas canvas); - - /** - * 滚动动画 - * 必须放在computeScroll()方法中执行 - */ - public abstract void scrollAnim(); - - /** - * 取消动画 - */ - public abstract void abortAnim(); - - /** - * 获取背景板 - * @return - */ - public abstract Bitmap getBgBitmap(); - - /** - * 获取内容显示版面 - */ - public abstract Bitmap getNextBitmap(); - - public enum Direction { - NONE(true),NEXT(true), PRE(true), UP(false), DOWN(false); - - public final boolean isHorizontal; - - Direction(boolean isHorizontal) { - this.isHorizontal = isHorizontal; - } - } - - public interface OnPageChangeListener { - boolean hasPrev(); - boolean hasNext(); - void pageCancel(); - } - -} diff --git a/app/src/main/java/com/novel/read/widget/page/PageLoader.java b/app/src/main/java/com/novel/read/widget/page/PageLoader.java deleted file mode 100644 index ee5b200..0000000 --- a/app/src/main/java/com/novel/read/widget/page/PageLoader.java +++ /dev/null @@ -1,1552 +0,0 @@ -package com.novel.read.widget.page; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; - -import android.text.TextPaint; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.target.SimpleTarget; -import com.bumptech.glide.request.transition.Transition; -import com.novel.read.R; -import com.novel.read.constants.Constant; -import com.novel.read.model.db.BookRecordBean; -import com.novel.read.model.db.CollBookBean; -import com.novel.read.model.db.dbManage.BookRepository; -import com.novel.read.utlis.DateUtli; -import com.novel.read.utlis.IOUtils; -import com.novel.read.utlis.RxUtils; -import com.novel.read.utlis.ScreenUtils; -import com.novel.read.utlis.StringUtils; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.reactivex.Single; -import io.reactivex.SingleObserver; -import io.reactivex.SingleOnSubscribe; -import io.reactivex.disposables.Disposable; - -/** - * Created by zlj - */ -public abstract class PageLoader { - private static final String TAG = "PageLoader"; - - // 当前页面的状态 - public static final int STATUS_LOADING = 1; // 正在加载 - public static final int STATUS_FINISH = 2; // 加载完成 - public static final int STATUS_ERROR = 3; // 加载错误 (一般是网络加载情况) - public static final int STATUS_EMPTY = 4; // 空数据 - public static final int STATUS_PARING = 5; // 正在解析 (装载本地数据) - public static final int STATUS_PARSE_ERROR = 6; // 本地文件解析错误(暂未被使用) - public static final int STATUS_CATEGORY_EMPTY = 7; // 获取到的目录为空 - // 默认的显示参数配置 - private static final int DEFAULT_MARGIN_HEIGHT = 45; - private static final int DEFAULT_MARGIN_WIDTH = 15; - private static final int DEFAULT_TIP_SIZE = 12; - private static final int EXTRA_TITLE_SIZE = 4; - - // 当前章节列表 - protected List mChapterList; - // 书本对象 - protected CollBookBean mCollBook; - // 监听器 - protected OnPageChangeListener mPageChangeListener; - - private Context mContext; - // 页面显示类 - private PageView mPageView; - // 当前显示的页 - private TxtPage mCurPage; - // 上一章的页面列表缓存 - private List mPrePageList; - // 当前章节的页面列表 - private List mCurPageList; - // 下一章的页面列表缓存 - private List mNextPageList; - - // 绘制电池的画笔 - private Paint mBatteryPaint; - // 绘制提示的画笔 - private Paint mTipPaint; - // 绘制标题的画笔 - private Paint mTitlePaint; - // 绘制背景颜色的画笔(用来擦除需要重绘的部分) - private Paint mBgPaint; - // 绘制小说内容的画笔 - private TextPaint mTextPaint; - private Paint mSelectPaint; - // 阅读器的配置选项 - private ReadSettingManager mSettingManager; - // 被遮盖的页,或者认为被取消显示的页 - private TxtPage mCancelPage; - // 存储阅读记录类 - private BookRecordBean mBookRecord; - - private Disposable mPreLoadDisp; - - /*****************params**************************/ - // 当前的状态 - protected int mStatus = STATUS_LOADING; - // 判断章节列表是否加载完成 - protected boolean isChapterListPrepare; - - // 是否打开过章节 - private boolean isChapterOpen; - private boolean isFirstOpen = true; - private boolean isClose; - // 页面的翻页效果模式 - private PageMode mPageMode; - // 加载器的颜色主题 - private PageStyle mPageStyle; - //当前是否是夜间模式 - private boolean isNightMode; - //书籍绘制区域的宽高 - private int mVisibleWidth; - private int mVisibleHeight; - //应用的宽高 - private int mDisplayWidth; - private int mDisplayHeight; - //间距 - private int mMarginWidth; - private int mMarginHeight; - //字体的颜色 - private int mTextColor; - //标题的大小 - private int mTitleSize; - //字体的大小 - private int mTextSize; - //行间距 - private int mTextInterval; - //标题的行间距 - private int mTitleInterval; - //段落距离(基于行间距的额外距离) - private int mTextPara; - private int mTitlePara; - //电池的百分比 - private int mBatteryLevel; - //当前页面的背景 - private int mBgColor; - - // 当前章 - protected int mCurChapterPos = 0; - //上一章的记录 - private int mLastChapterPos = 0; - - /*****************************init params*******************************/ - public PageLoader(PageView pageView, CollBookBean collBook) { - mPageView = pageView; - mContext = pageView.getContext(); - mCollBook = collBook; - mChapterList = new ArrayList<>(1); - - // 初始化数据 - initData(); - // 初始化画笔 - initPaint(); - // 初始化PageView - initPageView(); - // 初始化书籍 - prepareBook(); - } - - private void initData() { - // 获取配置管理器 - mSettingManager = ReadSettingManager.Companion.getInstance(); - // 获取配置参数 - mPageMode = mSettingManager.getPageMode(); - mPageStyle = mSettingManager.getPageStyle(); - // 初始化参数 - mMarginWidth = ScreenUtils.INSTANCE.dpToPx(DEFAULT_MARGIN_WIDTH); - mMarginHeight = ScreenUtils.INSTANCE.dpToPx(DEFAULT_MARGIN_HEIGHT); - // 配置文字有关的参数 - setUpTextParams(mSettingManager.getTextSize()); - } - - /** - * 作用:设置与文字相关的参数 - * - * @param textSize - */ - private void setUpTextParams(int textSize) { - // 文字大小 - mTextSize = textSize; - mTitleSize = mTextSize + ScreenUtils.INSTANCE.spToPx(EXTRA_TITLE_SIZE); - // 行间距(大小为字体的一半) - mTextInterval = mTextSize / 2; - mTitleInterval = mTitleSize / 2; - // 段落间距(大小为字体的高度) - mTextPara = mTextSize; - mTitlePara = mTitleSize; - } - - private void initPaint() { - // 绘制提示的画笔 - mTipPaint = new Paint(); - mTipPaint.setColor(mTextColor); - mTipPaint.setTextAlign(Paint.Align.LEFT); // 绘制的起始点 - mTipPaint.setTextSize(ScreenUtils.INSTANCE.spToPx(DEFAULT_TIP_SIZE)); // Tip默认的字体大小 - mTipPaint.setAntiAlias(true); - mTipPaint.setSubpixelText(true); - - // 绘制页面内容的画笔 - mTextPaint = new TextPaint(); - mTextPaint.setColor(mTextColor); - mTextPaint.setTextSize(mTextSize); - mTextPaint.setAntiAlias(true); - - mSelectPaint = new TextPaint(); - mSelectPaint.setColor(mContext.getResources().getColor(R.color.colorSelect)); - mSelectPaint.setTextSize(mTextSize); - mSelectPaint.setAntiAlias(true); - - // 绘制标题的画笔 - mTitlePaint = new TextPaint(); - mTitlePaint.setColor(mTextColor); - mTitlePaint.setTextSize(mTitleSize); - mTitlePaint.setStyle(Paint.Style.FILL_AND_STROKE); - mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD); - mTitlePaint.setAntiAlias(true); - - // 绘制背景的画笔 - mBgPaint = new Paint(); - mBgPaint.setColor(mBgColor); - - // 绘制电池的画笔 - mBatteryPaint = new Paint(); - mBatteryPaint.setAntiAlias(true); - mBatteryPaint.setDither(true); - - // 初始化页面样式 - setNightMode(mSettingManager.isNightMode()); - } - - private void initPageView() { - //配置参数 - mPageView.setPageMode(mPageMode); - mPageView.setBgColor(mBgColor); - } - - /** - * 跳转到上一章 - * - * @return - */ - public boolean skipPreChapter() { - if (!hasPrevChapter()) { - return false; - } - - // 载入上一章。 - if (parsePrevChapter()) { - mCurPage = getCurPage(0); - } else { - mCurPage = new TxtPage(); - } - mPageView.drawCurPage(false); - return true; - } - - /** - * 跳转到下一章 - */ - public boolean skipNextChapter() { - if (!hasNextChapter()) { - return false; - } - - //判断是否达到章节的终止点 - if (parseNextChapter()) { - mCurPage = getCurPage(0); - } else { - mCurPage = new TxtPage(); - } - mPageView.drawCurPage(false); - return true; - } - - /** - * 跳转到指定章节 - * - * @param pos:从 0 开始。 - */ - public void skipToChapter(int pos) { - // 设置参数 - mCurChapterPos = pos; - - // 将上一章的缓存设置为null - mPrePageList = null; - // 如果当前下一章缓存正在执行,则取消 - if (mPreLoadDisp != null) { - mPreLoadDisp.dispose(); - } - // 将下一章缓存设置为null - mNextPageList = null; - - // 打开指定章节 - openChapter(); - } - - /** - * 跳转到指定的页 - * - * @param pos - */ - public boolean skipToPage(int pos) { - if (!isChapterListPrepare) { - return false; - } - mCurPage = getCurPage(pos); - mPageView.drawCurPage(false); - return true; - } - - /** - * 翻到上一页 - */ - public boolean skipToPrePage() { - return mPageView.autoPrevPage(); - } - - /** - * 翻到下一页 - */ - public boolean skipToNextPage() { - return mPageView.autoNextPage(); - } - - /** - * 更新时间 - */ - public void updateTime() { - if (!mPageView.isRunning()) { - mPageView.drawCurPage(true); - } - } - - /** - * 更新电量 - * - * @param level - */ - public void updateBattery(int level) { - mBatteryLevel = level; - - if (!mPageView.isRunning()) { - mPageView.drawCurPage(true); - } - } - - /** - * 设置提示的文字大小 - * - * @param textSize:单位为 px。 - */ - public void setTipTextSize(int textSize) { - mTipPaint.setTextSize(textSize); - - // 如果屏幕大小加载完成 - mPageView.drawCurPage(false); - } - - /** - * 设置文字相关参数 - * - * @param textSize - */ - public void setTextSize(int textSize) { - // 设置文字相关参数 - setUpTextParams(textSize); - - // 设置画笔的字体大小 - mTextPaint.setTextSize(mTextSize); - mSelectPaint.setTextSize(mTextSize); - // 设置标题的字体大小 - mTitlePaint.setTextSize(mTitleSize); - // 存储文字大小 - mSettingManager.setTextSize(mTextSize); - // 取消缓存 - mPrePageList = null; - mNextPageList = null; - - // 如果当前已经显示数据 - if (isChapterListPrepare && mStatus == STATUS_FINISH) { - // 重新计算当前页面 - dealLoadPageList(mCurChapterPos); - - // 防止在最后一页,通过修改字体,以至于页面数减少导致崩溃的问题 - if (mCurPage.position >= mCurPageList.size()) { - mCurPage.position = mCurPageList.size() - 1; - } - - // 重新获取指定页面 - mCurPage = mCurPageList.get(mCurPage.position); - } - - mPageView.drawCurPage(false); - } - - /** - * 设置夜间模式 - * - * @param nightMode - */ - public void setNightMode(boolean nightMode) { - mSettingManager.setNightMode(nightMode); - isNightMode = nightMode; - - if (isNightMode) { - mBatteryPaint.setColor(Color.WHITE); - setPageStyle(PageStyle.NIGHT); - } else { - mBatteryPaint.setColor(Color.BLACK); - setPageStyle(mPageStyle); - } - } - - /** - * 设置页面样式 - * - * @param pageStyle:页面样式 - */ - public void setPageStyle(PageStyle pageStyle) { - if (pageStyle != PageStyle.NIGHT) { - mPageStyle = pageStyle; - mSettingManager.setPageStyle(pageStyle); - } - - if (isNightMode && pageStyle != PageStyle.NIGHT) { - return; - } - - // 设置当前颜色样式 - mTextColor = ContextCompat.getColor(mContext, pageStyle.getFontColor()); - mBgColor = ContextCompat.getColor(mContext, pageStyle.getBgColor()); - - mTipPaint.setColor(mTextColor); - mTitlePaint.setColor(mTextColor); - mTextPaint.setColor(mTextColor); - - mBgPaint.setColor(mBgColor); - - mPageView.drawCurPage(false); - } - - /** - * 翻页动画 - * - * @param pageMode:翻页模式 - * @see PageMode - */ - public void setPageMode(PageMode pageMode) { - mPageMode = pageMode; - - mPageView.setPageMode(mPageMode); - mSettingManager.setPageMode(mPageMode); - - // 重新绘制当前页 - mPageView.drawCurPage(false); - } - - /** - * 设置内容与屏幕的间距 - * - * @param marginWidth :单位为 px - * @param marginHeight :单位为 px - */ - public void setMargin(int marginWidth, int marginHeight) { - mMarginWidth = marginWidth; - mMarginHeight = marginHeight; - - // 如果是滑动动画,则需要重新创建了 - if (mPageMode == PageMode.SCROLL) { - mPageView.setPageMode(PageMode.SCROLL); - } - - mPageView.drawCurPage(false); - } - - /** - * 设置页面切换监听 - * - * @param listener - */ - public void setOnPageChangeListener(OnPageChangeListener listener) { - mPageChangeListener = listener; - - // 如果目录加载完之后才设置监听器,那么会默认回调 - if (isChapterListPrepare) { - mPageChangeListener.onCategoryFinish(mChapterList); - } - } - - /** - * 获取当前页的状态 - * - * @return - */ - public int getPageStatus() { - return mStatus; - } - - /** - * 获取书籍信息 - * - * @return - */ - public CollBookBean getCollBook() { - return mCollBook; - } - - /** - * 获取章节目录。 - * - * @return - */ - public List getChapterCategory() { - return mChapterList; - } - - public TxtPage getCurPage() { - return mCurPage; - } - - public void setCurPage(TxtPage mCurPage) { - this.mCurPage = mCurPage; - } - - public List getCurPageList() { - if (mCurPageList == null) { - return new ArrayList<>(); - } - return mCurPageList; - } - - public List getNextPageList() { - if (mNextPageList == null) { - return new ArrayList<>(); - } - return mNextPageList; - } - - /** - * 获取当前页的页码 - */ - public int getPagePos() { - return mCurPage.position; - } - - /** - * 获取当前章节的章节位置 - */ - public int getChapterPos() { - return mCurChapterPos; - } - - /** - * 获取距离屏幕的高度 - */ - public int getMarginHeight() { - return mMarginHeight; - } - - /** - * 保存阅读记录 - */ - public void saveRecord() { - - if (mChapterList.isEmpty()) { - return; - } - - mBookRecord.setBookId(mCollBook.getId()); - mBookRecord.setChapter(mCurChapterPos); - - if (mCurPage != null) { - mBookRecord.setPagePos(mCurPage.position); - } else { - mBookRecord.setPagePos(0); - } - - //存储到数据库 - BookRepository.getInstance().saveBookRecord(mBookRecord); - } - - /** - * 初始化书籍 - */ - private void prepareBook() { - mBookRecord = BookRepository.getInstance() - .getBookRecord(mCollBook.getId()); - - if (mBookRecord == null) { - mBookRecord = new BookRecordBean(); - } - - mCurChapterPos = mBookRecord.getChapter(); - mLastChapterPos = mCurChapterPos; - } - - /** - * 打开指定章节 - */ - public void openChapter() { - isFirstOpen = false; - - if (!mPageView.isPrepare()) { - return; - } - - // 如果章节目录没有准备好 - if (!isChapterListPrepare) { - mStatus = STATUS_LOADING; - mPageView.drawCurPage(false); - return; - } - - // 如果获取到的章节目录为空 - if (mChapterList.isEmpty()) { - mStatus = STATUS_CATEGORY_EMPTY; - mPageView.drawCurPage(false); - return; - } - - if (parseCurChapter()) { - // 如果章节从未打开 - if (!isChapterOpen) { - int position = mBookRecord.getPagePos(); - - // 防止记录页的页号,大于当前最大页号 - if (position >= mCurPageList.size()) { - position = mCurPageList.size() - 1; - } - mCurPage = getCurPage(position); - mCancelPage = mCurPage; - // 切换状态 - isChapterOpen = true; - } else { - mCurPage = getCurPage(0); - } - } else { - mCurPage = new TxtPage(); - } - - mPageView.drawCurPage(false); - } - - public void chapterError() { - //加载错误 - mStatus = STATUS_ERROR; - mPageView.drawCurPage(false); - } - - /** - * 关闭书本 - */ - public void closeBook() { - isChapterListPrepare = false; - isClose = true; - - if (mPreLoadDisp != null) { - mPreLoadDisp.dispose(); - } - - clearList(mChapterList); - clearList(mCurPageList); - clearList(mNextPageList); - - mChapterList = null; - mCurPageList = null; - mNextPageList = null; - mPageView = null; - mCurPage = null; - } - - private void clearList(List list) { - if (list != null) { - list.clear(); - } - } - - public boolean isClose() { - return isClose; - } - - public boolean isChapterOpen() { - return isChapterOpen; - } - - /** - * 加载页面列表 - * - * @param chapterPos:章节序号 - * @return - */ - private List loadPageList(int chapterPos) throws Exception { - // 获取章节 - TxtChapter chapter = mChapterList.get(chapterPos); - // 判断章节是否存在 - if (!hasChapterData(chapter)) { - return null; - } - // 获取章节的文本流 - BufferedReader reader = getChapterReader(chapter); - - return loadPages(chapter, reader); - } - - /** - * 刷新章节列表 - */ - public abstract void refreshChapterList(); - - /** - * 获取章节的文本流 - */ - protected abstract BufferedReader getChapterReader(TxtChapter chapter) throws Exception; - - /** - * 章节数据是否存在 - */ - protected abstract boolean hasChapterData(TxtChapter chapter); - - /***********************************default method***********************************************/ - - void drawPage(Bitmap bitmap, boolean isUpdate) { - drawBackground(mPageView.getBgBitmap(), isUpdate); - if (!isUpdate) { - drawContent(bitmap); - } - //更新绘制 - mPageView.invalidate(); - } - - private void drawBackground(Bitmap bitmap, boolean isUpdate) { - Canvas canvas = new Canvas(bitmap); - int tipMarginHeight = ScreenUtils.INSTANCE.dpToPx(3); - if (!isUpdate) { - //绘制背景 - canvas.drawColor(mBgColor); - - if (!mChapterList.isEmpty()) { - //初始化标题的参数 - //需要注意的是:绘制text的y的起始点是text的基准线的位置,而不是从text的头部的位置 - float tipTop = tipMarginHeight - mTipPaint.getFontMetrics().top; - //根据状态不一样,数据不一样 - if (mStatus != STATUS_FINISH) { - if (isChapterListPrepare) { - //todo 目前不清楚发生的情形,只能这样防止用户瞎逼操作导致数组越界 - if (mChapterList.size() > mCurChapterPos) { - canvas.drawText(mChapterList.get(mCurChapterPos).getTitle() - , mMarginWidth, tipTop, mTipPaint); - } - } - } else { - canvas.drawText(mCurPage.title, mMarginWidth, tipTop, mTipPaint); - } - - //绘制页码 - // 底部的字显示的位置Y - float y = mDisplayHeight - mTipPaint.getFontMetrics().bottom - tipMarginHeight; - // 只有finish的时候采用页码 - if (mStatus == STATUS_FINISH) { - String percent = (mCurPage.position + 1) + "/" + mCurPageList.size(); - canvas.drawText(percent, mMarginWidth, y, mTipPaint); - } - } - } else { - //擦除区域 - mBgPaint.setColor(mBgColor); - canvas.drawRect(mDisplayWidth >> 1, mDisplayHeight - mMarginHeight + ScreenUtils.INSTANCE.dpToPx(2), mDisplayWidth, mDisplayHeight, mBgPaint); - } - - //绘制电池 - int visibleRight = mDisplayWidth - mMarginWidth; - int visibleBottom = mDisplayHeight - tipMarginHeight; - - int outFrameWidth = (int) mTipPaint.measureText("xxx"); - int outFrameHeight = (int) mTipPaint.getTextSize(); - - int polarHeight = ScreenUtils.INSTANCE.dpToPx(6); - int polarWidth = ScreenUtils.INSTANCE.dpToPx(2); - int border = 1; - int innerMargin = 1; - - //电极的制作 - int polarLeft = visibleRight - polarWidth; - int polarTop = visibleBottom - (outFrameHeight + polarHeight) / 2; - Rect polar = new Rect(polarLeft, polarTop, visibleRight, - polarTop + polarHeight - ScreenUtils.INSTANCE.dpToPx(2)); - - mBatteryPaint.setStyle(Paint.Style.FILL); - canvas.drawRect(polar, mBatteryPaint); - - //外框的制作 - int outFrameLeft = polarLeft - outFrameWidth; - int outFrameTop = visibleBottom - outFrameHeight; - int outFrameBottom = visibleBottom - ScreenUtils.INSTANCE.dpToPx(2); - Rect outFrame = new Rect(outFrameLeft, outFrameTop, polarLeft, outFrameBottom); - - mBatteryPaint.setStyle(Paint.Style.STROKE); - mBatteryPaint.setStrokeWidth(border); - canvas.drawRect(outFrame, mBatteryPaint); - - //内框的制作 - float innerWidth = (outFrame.width() - innerMargin * 2 - border) * (mBatteryLevel / 100.0f); - RectF innerFrame = new RectF(outFrameLeft + border + innerMargin, outFrameTop + border + innerMargin, - outFrameLeft + border + innerMargin + innerWidth, outFrameBottom - border - innerMargin); - - mBatteryPaint.setStyle(Paint.Style.FILL); - canvas.drawRect(innerFrame, mBatteryPaint); - - //绘制当前时间 - //底部的字显示的位置Y - float y = mDisplayHeight - mTipPaint.getFontMetrics().bottom - tipMarginHeight; - String time = DateUtli.INSTANCE.dateConvert(System.currentTimeMillis(), Constant.FORMAT_TIME); - float x = outFrameLeft - mTipPaint.measureText(time) - ScreenUtils.INSTANCE.dpToPx(4); - canvas.drawText(time, x, y, mTipPaint); - } - - private void drawContent(Bitmap bitmap) { - Canvas canvas = new Canvas(bitmap); - - if (mPageMode == PageMode.SCROLL) { - canvas.drawColor(mBgColor); - } - - //绘制内容 - if (mStatus != STATUS_FINISH) { - //绘制字体 - String tip = ""; - switch (mStatus) { - case STATUS_LOADING: - tip = "正在拼命加载中..."; - break; - case STATUS_ERROR: - tip = "加载失败(点击边缘重试)"; - break; - case STATUS_EMPTY: - tip = "文章内容为空"; - break; - case STATUS_PARING: - tip = "正在排版请等待..."; - break; - case STATUS_PARSE_ERROR: - tip = "文件解析错误"; - break; - case STATUS_CATEGORY_EMPTY: - tip = "目录列表为空"; - break; - } - - //将提示语句放到正中间 - drawCenter(tip, canvas); - } else { - float top; - - if (mPageMode == PageMode.SCROLL) { - top = -mTextPaint.getFontMetrics().top; - } else { - top = mMarginHeight - mTextPaint.getFontMetrics().top; - } - - //设置总距离 - int interval = mTextInterval + (int) mTextPaint.getTextSize(); - int para = mTextPara + (int) mTextPaint.getTextSize(); - int titleInterval = mTitleInterval + (int) mTitlePaint.getTextSize(); - int titlePara = mTitlePara + (int) mTextPaint.getTextSize(); - String str; - - //对标题进行绘制 - for (int i = 0; i < mCurPage.titleLines; ++i) { - str = mCurPage.getLines().get(i); - //设置顶部间距 - if (i == 0) { - top += mTitlePara; - } - - //计算文字显示的起始点 - int start = (int) (mDisplayWidth - mTitlePaint.measureText(str)) / 2; - //进行绘制 - mTitlePaint.setColor(mTextColor); - canvas.drawText(str, start, top, mTitlePaint); - - //设置尾部间距 - if (i == mCurPage.titleLines - 1) { - top += titlePara; - } else { - //行间距 - top += titleInterval; - } - } - - //对内容进行绘制 - for (int i = mCurPage.titleLines; i < mCurPage.lines.size(); ++i) { - str = mCurPage.getLines().get(i); -// if (i == 0) { -// top = top + 15; -// } - - canvas.drawText(str, mMarginWidth, top, mTextPaint); - - if (str.endsWith("\n")) { - top += para; - } else { - top += interval; - } - } - - if (!TextUtils.isEmpty(getCurPage().getPic())) { - Glide.with(mContext).asBitmap().load(getCurPage().getPic()).thumbnail(0.1f).into(new SimpleTarget() { - @Override - public void onLoadStarted(@Nullable Drawable placeholder) { - canvas.save(); - drawCenter(mContext.getString(R.string.pic_loading), canvas); - canvas.restore(); - } - - @Override - public void onResourceReady(@NonNull Bitmap resource, Transition transition) { - - if (resource.getWidth() > mDisplayWidth) { - resource = scaleBitmap(resource); - } - float pivotX = (mDisplayWidth - resource.getWidth()) >> 1; - float pivotY = (mDisplayHeight - resource.getHeight()) >> 1; - if (!TextUtils.isEmpty(getCurPage().getPic())) { - canvas.drawBitmap(resource, pivotX, pivotY, mTextPaint); - mPageView.invalidate(); - } - - } - }); - } - - } - } - - //中心文字绘制 - private void drawCenter(String tip, Canvas canvas) { - Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); - float textHeight = fontMetrics.top - fontMetrics.bottom; - float textWidth = mTextPaint.measureText(tip); - float pivotX = (mDisplayWidth - textWidth) / 2; - float pivotY = (mDisplayHeight - textHeight) / 2; - canvas.drawText(tip, pivotX, pivotY, mTextPaint); - } - - - //图片缩放 - private Bitmap scaleBitmap(Bitmap origin) { - if (origin == null) { - return null; - } - int width = origin.getWidth(); - int height = origin.getHeight(); - Matrix matrix = new Matrix(); - matrix.preScale((float) 0.5, (float) 0.5); - Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false); - if (newBM.equals(origin)) { - return newBM; - } - return newBM; - } - - void prepareDisplay(int w, int h) { - // 获取PageView的宽高 - mDisplayWidth = w; - mDisplayHeight = h; - - // 获取内容显示位置的大小 - mVisibleWidth = mDisplayWidth - mMarginWidth * 2; - mVisibleHeight = mDisplayHeight - mMarginHeight * 2; - - // 重置 PageMode - mPageView.setPageMode(mPageMode); - - if (!isChapterOpen) { - // 展示加载界面 - mPageView.drawCurPage(false); - // 如果在 display 之前调用过 openChapter 肯定是无法打开的。 - // 所以需要通过 display 再重新调用一次。 - if (!isFirstOpen) { - // 打开书籍 - openChapter(); - } - } else { - // 如果章节已显示,那么就重新计算页面 - if (mStatus == STATUS_FINISH) { - dealLoadPageList(mCurChapterPos); - // 重新设置文章指针的位置 - mCurPage = getCurPage(mCurPage.position); - } - mPageView.drawCurPage(false); - } - } - - /** - * 翻阅上一页 - */ - boolean prev() { - // 以下情况禁止翻页 - if (!canTurnPage()) { - return false; - } - - if (mStatus == STATUS_FINISH) { - // 先查看是否存在上一页 - TxtPage prevPage = getPrevPage(); - if (prevPage != null) { - mCancelPage = mCurPage; - mCurPage = prevPage; - mPageView.drawNextPage(); - return true; - } - } - - if (!hasPrevChapter()) { - return false; - } - - mCancelPage = mCurPage; - if (parsePrevChapter()) { - mCurPage = getPrevLastPage(); - } else { - mCurPage = new TxtPage(); - } - mPageView.drawNextPage(); - return true; - } - - /** - * 解析上一章数据 - * - * @return :数据是否解析成功 - */ - boolean parsePrevChapter() { - // 加载上一章数据 - int prevChapter = mCurChapterPos - 1; - - mLastChapterPos = mCurChapterPos; - mCurChapterPos = prevChapter; - - // 当前章缓存为下一章 - mNextPageList = mCurPageList; - - // 判断是否具有上一章缓存 - if (mPrePageList != null) { - mCurPageList = mPrePageList; - mPrePageList = null; - - // 回调 - chapterChangeCallback(); - } else { - dealLoadPageList(prevChapter); - } - return mCurPageList != null; - } - - private boolean hasPrevChapter() { - //判断是否上一章节为空 - return mCurChapterPos - 1 >= 0; - } - - /** - * 翻到下一页 - * - * @return :是否允许翻页 - */ - boolean next() { - // 以下情况禁止翻页 - if (!canTurnPage()) { - return false; - } - - if (mStatus == STATUS_FINISH) { - // 先查看是否存在下一页 - TxtPage nextPage = getNextPage(); - if (nextPage != null) { - mCancelPage = mCurPage; - mCurPage = nextPage; - mPageView.drawNextPage(); - return true; - } - } - - if (!hasNextChapter()) { - return false; - } - - mCancelPage = mCurPage; - // 解析下一章数据 - if (parseNextChapter()) { - mCurPage = mCurPageList.get(0); - } else { - mCurPage = new TxtPage(); - } - mPageView.drawNextPage(); - return true; - } - - private boolean hasNextChapter() { - // 判断是否到达目录最后一章 - return mCurChapterPos + 1 < mChapterList.size(); - } - - boolean parseCurChapter() { - // 解析数据 - dealLoadPageList(mCurChapterPos); - // 预加载下一页面 - preLoadNextChapter(); - return mCurPageList != null; - } - - /** - * 解析下一章数据 - * - * @return:返回解析成功还是失败 - */ - boolean parseNextChapter() { - int nextChapter = mCurChapterPos + 1; - - mLastChapterPos = mCurChapterPos; - mCurChapterPos = nextChapter; - - // 将当前章的页面列表,作为上一章缓存 - mPrePageList = mCurPageList; - - // 是否下一章数据已经预加载了 - if (mNextPageList != null) { - mCurPageList = mNextPageList; - mNextPageList = null; - // 回调 - chapterChangeCallback(); - } else { - // 处理页面解析 - dealLoadPageList(nextChapter); - } - // 预加载下一页面 - preLoadNextChapter(); - return mCurPageList != null; - } - - private void dealLoadPageList(int chapterPos) { - try { - mCurPageList = loadPageList(chapterPos); - if (mCurPageList != null) { - if (mCurPageList.isEmpty()) { - mStatus = STATUS_EMPTY; - - // 添加一个空数据 - TxtPage page = new TxtPage(); - page.lines = new ArrayList<>(1); - mCurPageList.add(page); - } else { - mStatus = STATUS_FINISH; - } - } else { - mStatus = STATUS_LOADING; - } - } catch (Exception e) { - e.printStackTrace(); - - mCurPageList = null; - mStatus = STATUS_ERROR; - } - // 回调 - chapterChangeCallback(); - } - - private void chapterChangeCallback() { - - if (mPageChangeListener != null) { - mPageChangeListener.onChapterChange(mCurChapterPos); - pics.clear(); - mPageChangeListener.onPageCountChange(mCurPageList != null ? mCurPageList.size() : 0); - } - } - - // 预加载下一章 - private void preLoadNextChapter() { - int nextChapter = mCurChapterPos + 1; - - // 如果不存在下一章,且下一章没有数据,则不进行加载。 - if (!hasNextChapter() - || !hasChapterData(mChapterList.get(nextChapter))) { - return; - } - - //如果之前正在加载则取消 - if (mPreLoadDisp != null) { - mPreLoadDisp.dispose(); - } - - //调用异步进行预加载加载 - Single.create((SingleOnSubscribe>) e -> e.onSuccess(loadPageList(nextChapter))).compose(RxUtils::toSimpleSingle) - .subscribe(new SingleObserver>() { - @Override - public void onSubscribe(Disposable d) { - mPreLoadDisp = d; - } - - @Override - public void onSuccess(List pages) { - mNextPageList = pages; - } - - @Override - public void onError(Throwable e) { - //无视错误 - } - }); - } - - // 取消翻页 - void pageCancel() { - if (mCurPage.position == 0 && mCurChapterPos > mLastChapterPos) { // 加载到下一章取消了 - if (mPrePageList != null) { - cancelNextChapter(); - } else { - if (parsePrevChapter()) { - mCurPage = getPrevLastPage(); - } else { - mCurPage = new TxtPage(); - } - } - } else if (mCurPageList == null - || (mCurPage.position == mCurPageList.size() - 1 - && mCurChapterPos < mLastChapterPos)) { // 加载上一章取消了 - - if (mNextPageList != null) { - cancelPreChapter(); - } else { - if (parseNextChapter()) { - mCurPage = mCurPageList.get(0); - } else { - mCurPage = new TxtPage(); - } - } - } else { - // 假设加载到下一页,又取消了。那么需要重新装载。 - mCurPage = mCancelPage; - } - } - - private void cancelNextChapter() { - int temp = mLastChapterPos; - mLastChapterPos = mCurChapterPos; - mCurChapterPos = temp; - - mNextPageList = mCurPageList; - mCurPageList = mPrePageList; - mPrePageList = null; - - chapterChangeCallback(); - - mCurPage = getPrevLastPage(); - mCancelPage = null; - } - - private void cancelPreChapter() { - // 重置位置点 - int temp = mLastChapterPos; - mLastChapterPos = mCurChapterPos; - mCurChapterPos = temp; - // 重置页面列表 - mPrePageList = mCurPageList; - mCurPageList = mNextPageList; - mNextPageList = null; - - chapterChangeCallback(); - - mCurPage = getCurPage(0); - mCancelPage = null; - } - - /** - * 将章节数据,解析成页面列表 - * chapter:章节信息 - * br:章节的文本流 - */ - private List pics = new ArrayList<>(); - - private List loadPages(TxtChapter chapter, BufferedReader br) { - - //生成的页面 - List pages = new ArrayList<>(); - //使用流的方式加载 - List lines = new ArrayList<>(); - int rHeight = mVisibleHeight; - int titleLinesCount = 0; - boolean showTitle = true; // 是否展示标题 - String paragraph = chapter.getTitle();//默认展示标题 - String title = StringUtils.INSTANCE.convertCC(chapter.getTitle()); - String half; - try { - while (showTitle || (paragraph = br.readLine()) != null) { - half = paragraph; - paragraph = StringUtils.INSTANCE.convertCC(paragraph); - // 重置段落 - if (!showTitle) { - paragraph = paragraph.replaceAll("\\s", ""); - // 如果只有换行符,那么就不执行 - if (paragraph.equals("")) continue; - paragraph = StringUtils.INSTANCE.halfToFull(" " + paragraph + "\n"); - } else { - //设置 title 的顶部间距 - rHeight -= mTitlePara; - } - - int wordCount; - String subStr; - while (paragraph.length() > 0) { - //当前空间,是否容得下一行文字 - if (showTitle) { - rHeight -= mTitlePaint.getTextSize(); - } else { - rHeight -= mTextPaint.getTextSize(); - } - - // 一页已经填充满了,创建 TextPage - if (rHeight <= 0) { - // 创建Page - TxtPage page = new TxtPage(); - page.position = pages.size(); - page.title = title; - page.lines = new ArrayList<>(lines); - page.titleLines = titleLinesCount; - pages.add(page); - // 重置Lines - lines.clear(); - rHeight = mVisibleHeight; - titleLinesCount = 0; - continue; - } - - //测量一行占用的字节数 - if (showTitle) { - wordCount = mTitlePaint.breakText(paragraph, - true, mVisibleWidth, null); - } else { - wordCount = mTextPaint.breakText(paragraph, - true, mVisibleWidth, null); - } - - subStr = paragraph.substring(0, wordCount); - if (!subStr.equals("\n")) { - //将一行字节,存储到lines中 - lines.add(subStr); - - //设置段落间距 - if (showTitle) { - titleLinesCount += 1; - rHeight -= mTitleInterval; - } else { - rHeight -= mTextInterval; - } - } - //裁剪 - paragraph = paragraph.substring(wordCount); - } - - //增加段落的间距 - if (!showTitle && lines.size() != 0) { - rHeight = rHeight - mTextPara + mTextInterval; - } - - if (showTitle) { - rHeight = rHeight - mTitlePara + mTitleInterval; - showTitle = false; - } - - pics.addAll(getImgs(half)); - } - if (lines.size() != 0) { - //创建Page - TxtPage page = new TxtPage(); - page.position = pages.size(); - page.title = title; - page.lines = new ArrayList<>(lines); - page.titleLines = titleLinesCount; - pages.add(page); - //重置Lines - lines.clear(); - } - - if (pics.size() > 0) { - for (int i = 0; i < pics.size(); i++) { - TxtPage page = new TxtPage(); - page.position = pages.size(); - page.title = title; - page.lines = new ArrayList<>(); - page.titleLines = 0; - page.setPic(pics.get(i)); - pages.add(page); - // 重置Lines - lines.clear(); - } - } - - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - IOUtils.INSTANCE.close(br); - } - return pages; - } - - - private List getImgs(String content) { - String img; - Pattern p_image; - Matcher m_image; - List images = new ArrayList<>(); - String regEx_img = "(]*?>)"; - p_image = Pattern.compile(regEx_img, Pattern.CASE_INSENSITIVE); - m_image = p_image.matcher(content); - while (m_image.find()) { - img = m_image.group(); - Matcher m = Pattern.compile("src\\s*=\\s*\"?(.*?)(\"|>|\\s+)").matcher(img); - while (m.find()) { - String tempSelected = m.group(1); - images.add(tempSelected); - } - } - - return images; - } - - /** - * @return :获取初始显示的页面 - */ - private TxtPage getCurPage(int pos) { - if (mPageChangeListener != null) { - mPageChangeListener.onPageChange(pos); - } - if (pos >= mCurPageList.size()) { - return mCurPageList.get(mCurPageList.size() - 1); - } - return mCurPageList.get(pos); - } - - /** - * @return :获取上一个页面 - */ - private TxtPage getPrevPage() { - int pos = mCurPage.position - 1; - if (pos < 0) { - return null; - } - if (mPageChangeListener != null) { - mPageChangeListener.onPageChange(pos); - } - return mCurPageList.get(pos); - } - - /** - * @return :获取下一的页面 - */ - private TxtPage getNextPage() { - int pos = mCurPage.position + 1; - if (pos >= mCurPageList.size()) { - return null; - } - if (mPageChangeListener != null) { - mPageChangeListener.onPageChange(pos); - } - return mCurPageList.get(pos); - } - - /** - * @return :获取上一个章节的最后一页 - */ - private TxtPage getPrevLastPage() { - int pos = mCurPageList.size() - 1; - - if (mPageChangeListener != null) { - mPageChangeListener.onPageChange(pos); - } - - return mCurPageList.get(pos); - } - - /** - * 根据当前状态,决定是否能够翻页 - */ - private boolean canTurnPage() { - - if (!isChapterListPrepare) { - return false; - } - - if (mStatus == STATUS_PARSE_ERROR - || mStatus == STATUS_PARING) { - return false; - } else if (mStatus == STATUS_ERROR) { - mStatus = STATUS_LOADING; - } - return true; - } - - /*****************************************interface*****************************************/ - - public interface OnPageChangeListener { - /** - * 作用:章节切换的时候进行回调 - * - * @param pos:切换章节的序号 - */ - void onChapterChange(int pos); - - /** - * 作用:请求加载章节内容 - * - * @param requestChapters:需要下载的章节列表 - */ - void requestChapters(List requestChapters); - - /** - * 作用:章节目录加载完成时候回调 - * - * @param chapters:返回章节目录 - */ - void onCategoryFinish(List chapters); - - /** - * 作用:章节页码数量改变之后的回调。==> 字体大小的调整,或者是否关闭虚拟按钮功能都会改变页面的数量。 - * - * @param count:页面的数量 - */ - void onPageCountChange(int count); - - /** - * 作用:当页面改变的时候回调 - * - * @param pos:当前的页面的序号 - */ - void onPageChange(int pos); - } - - -} diff --git a/app/src/main/java/com/novel/read/widget/page/PageMode.kt b/app/src/main/java/com/novel/read/widget/page/PageMode.kt deleted file mode 100644 index 92d9ac7..0000000 --- a/app/src/main/java/com/novel/read/widget/page/PageMode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.novel.read.widget.page - -/** - * Created by zlj - * 作用:翻页动画的模式 - */ -enum class PageMode { - SIMULATION, COVER, SLIDE, NONE, SCROLL -} diff --git a/app/src/main/java/com/novel/read/widget/page/PageStyle.kt b/app/src/main/java/com/novel/read/widget/page/PageStyle.kt deleted file mode 100644 index 6082c79..0000000 --- a/app/src/main/java/com/novel/read/widget/page/PageStyle.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.novel.read.widget.page - - -import androidx.annotation.ColorRes - -import com.novel.read.R - -/** - * Created by zlj - * 页面的展示风格。 - */ -enum class PageStyle private constructor(@param:ColorRes val fontColor: Int, @param:ColorRes val bgColor: Int) { - BG_0(R.color.read_font_one, R.color.read_bg_one), - BG_1(R.color.read_font_two, R.color.read_bg_two), - // BG_2(R.color.nb_read_font_3, R.color.nb_read_bg_3), - BG_3(R.color.read_font_four, R.color.read_bg_four), - BG_4(R.color.read_font_five, R.color.read_bg_five), - NIGHT(R.color.read_font_night, R.color.read_bg_night) -} diff --git a/app/src/main/java/com/novel/read/widget/page/PageView.java b/app/src/main/java/com/novel/read/widget/page/PageView.java deleted file mode 100644 index c62e47d..0000000 --- a/app/src/main/java/com/novel/read/widget/page/PageView.java +++ /dev/null @@ -1,364 +0,0 @@ -package com.novel.read.widget.page; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; - -import com.novel.read.model.db.CollBookBean; -import com.novel.read.widget.page.anim.CoverPageAnim; -import com.novel.read.widget.page.anim.HorizonPageAnim; -import com.novel.read.widget.page.anim.NonePageAnim; -import com.novel.read.widget.page.anim.ScrollPageAnim; -import com.novel.read.widget.page.anim.SimulationPageAnim; -import com.novel.read.widget.page.anim.SlidePageAnim; - -/** - * Created by Administrator on 2016/8/29 0029. - * 原作者的GitHub Project Path:(https://github.com/PeachBlossom/treader) - * 绘制页面显示内容的类 - */ -public class PageView extends View { - - private int mViewWidth = 0; // 当前View的宽 - private int mViewHeight = 0; // 当前View的高 - - private int mStartX = 0; - private int mStartY = 0; - private boolean isMove = false; - // 初始化参数 - private int mBgColor = 0xFFCEC29C; - private PageMode mPageMode = PageMode.SIMULATION; - // 是否允许点击 - private boolean canTouch = true; - // 唤醒菜单的区域 - private RectF mCenterRect = null; - private boolean isPrepare; - // 动画类 - private PageAnimation mPageAnim; - // 动画监听类 - private PageAnimation.OnPageChangeListener mPageAnimListener = new PageAnimation.OnPageChangeListener() { - @Override - public boolean hasPrev() { - return PageView.this.hasPrevPage(); - } - - @Override - public boolean hasNext() { - return PageView.this.hasNextPage(); - } - - @Override - public void pageCancel() { - PageView.this.pageCancel(); - } - }; - - //点击监听 - private TouchListener mTouchListener; - //内容加载器 - private PageLoader mPageLoader; - - public PageView(Context context) { - this(context, null); - } - - public PageView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PageView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - mViewWidth = w; - mViewHeight = h; - - isPrepare = true; - - if (mPageLoader != null) { - mPageLoader.prepareDisplay(w, h); - } - } - - //设置翻页的模式 - void setPageMode(PageMode pageMode) { - mPageMode = pageMode; - //视图未初始化的时候,禁止调用 - if (mViewWidth == 0 || mViewHeight == 0) return; - - switch (mPageMode) { - case SIMULATION: - mPageAnim = new SimulationPageAnim(mViewWidth, mViewHeight, this, mPageAnimListener); - break; - case COVER: - mPageAnim = new CoverPageAnim(mViewWidth, mViewHeight, this, mPageAnimListener); - break; - case SLIDE: - mPageAnim = new SlidePageAnim(mViewWidth, mViewHeight, this, mPageAnimListener); - break; - case NONE: - mPageAnim = new NonePageAnim(mViewWidth, mViewHeight, this, mPageAnimListener); - break; - case SCROLL: - mPageAnim = new ScrollPageAnim(mViewWidth, mViewHeight, 0, - mPageLoader.getMarginHeight(), this, mPageAnimListener); - break; - default: - mPageAnim = new SimulationPageAnim(mViewWidth, mViewHeight, this, mPageAnimListener); - } - } - - public Bitmap getNextBitmap() { - if (mPageAnim == null) return null; - return mPageAnim.getNextBitmap(); - } - - public Bitmap getBgBitmap() { - if (mPageAnim == null) return null; - return mPageAnim.getBgBitmap(); - } - - public boolean autoPrevPage() { - //滚动暂时不支持自动翻页 - if (mPageAnim instanceof ScrollPageAnim) { - return false; - } else { - startPageAnim(PageAnimation.Direction.PRE); - return true; - } - } - - public boolean autoNextPage() { - if (mPageAnim instanceof ScrollPageAnim) { - return false; - } else { - startPageAnim(PageAnimation.Direction.NEXT); - return true; - } - } - - private void startPageAnim(PageAnimation.Direction direction) { - if (mTouchListener == null) return; - //是否正在执行动画 - abortAnimation(); - if (direction == PageAnimation.Direction.NEXT) { - int x = mViewWidth; - int y = mViewHeight; - //初始化动画 - mPageAnim.setStartPoint(x, y); - //设置点击点 - mPageAnim.setTouchPoint(x, y); - //设置方向 - boolean hasNext = hasNextPage(); - - mPageAnim.setDirection(direction); - if (!hasNext) { - return; - } - } else { - int x = 0; - int y = mViewHeight; - //初始化动画 - mPageAnim.setStartPoint(x, y); - //设置点击点 - mPageAnim.setTouchPoint(x, y); - mPageAnim.setDirection(direction); - //设置方向方向 - boolean hashPrev = hasPrevPage(); - if (!hashPrev) { - return; - } - } - mPageAnim.startAnim(); - this.postInvalidate(); - } - - public void setBgColor(int color) { - mBgColor = color; - } - - @Override - protected void onDraw(Canvas canvas) { - - //绘制背景 - canvas.drawColor(mBgColor); - - //绘制动画 - mPageAnim.draw(canvas); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - super.onTouchEvent(event); - - if (!canTouch && event.getAction() != MotionEvent.ACTION_DOWN) return true; - - int x = (int) event.getX(); - int y = (int) event.getY(); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mStartX = x; - mStartY = y; - isMove = false; - canTouch = mTouchListener.onTouch(); - mPageAnim.onTouchEvent(event); - break; - case MotionEvent.ACTION_MOVE: - // 判断是否大于最小滑动值。 - int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); - if (!isMove) { - isMove = Math.abs(mStartX - event.getX()) > slop || Math.abs(mStartY - event.getY()) > slop; - } - - // 如果滑动了,则进行翻页。 - if (isMove) { - mPageAnim.onTouchEvent(event); - } - break; - case MotionEvent.ACTION_UP: - if (!isMove) { - //设置中间区域范围 - if (mCenterRect == null) { - mCenterRect = new RectF(mViewWidth / 5, mViewHeight / 3, - mViewWidth * 4 / 5, mViewHeight * 2 / 3); - } - - //是否点击了中间 - if (mCenterRect.contains(x, y)) { - if (mTouchListener != null) { - mTouchListener.center(); - } - return true; - } - } - mPageAnim.onTouchEvent(event); - break; - } - return true; - } - - /** - * 判断是否存在上一页 - */ - private boolean hasPrevPage() { - mTouchListener.prePage(); - return mPageLoader.prev(); - } - - /** - * 判断是否下一页存在 - */ - private boolean hasNextPage() { - mTouchListener.nextPage(); - return mPageLoader.next(); - } - - private void pageCancel() { - mTouchListener.cancel(); - mPageLoader.pageCancel(); - } - - @Override - public void computeScroll() { - //进行滑动 - mPageAnim.scrollAnim(); - super.computeScroll(); - } - - //如果滑动状态没有停止就取消状态,重新设置Anim的触碰点 - public void abortAnimation() { - mPageAnim.abortAnim(); - } - - public boolean isRunning() { - if (mPageAnim == null) { - return false; - } - return mPageAnim.isRunning(); - } - - public boolean isPrepare() { - return isPrepare; - } - - public void setTouchListener(TouchListener mTouchListener) { - this.mTouchListener = mTouchListener; - } - - public void drawNextPage() { - if (!isPrepare) return; - - if (mPageAnim instanceof HorizonPageAnim) { - ((HorizonPageAnim) mPageAnim).changePage(); - } - mPageLoader.drawPage(getNextBitmap(), false); - } - - /** - * 绘制当前页。 - */ - public void drawCurPage(boolean isUpdate) { - if (!isPrepare) return; - - if (!isUpdate) { - if (mPageAnim instanceof ScrollPageAnim) { - ((ScrollPageAnim) mPageAnim).resetBitmap(); - } - } - - mPageLoader.drawPage(getNextBitmap(), isUpdate); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mPageAnim.abortAnim(); - mPageAnim.clear(); - - mPageLoader = null; - mPageAnim = null; - } - - /** - * 获取 PageLoader - */ - public PageLoader getPageLoader(CollBookBean collBook) { - // 判是否已经存在 - if (mPageLoader != null) { - return mPageLoader; - } - // 根据书籍类型,获取具体的加载器 - if (collBook.isLocal()) { - mPageLoader = new LocalPageLoader(this, collBook); - } else { - mPageLoader = new NetPageLoader(this, collBook); - } - // 判断是否 PageView 已经初始化完成 - if (mViewWidth != 0 || mViewHeight != 0) { - // 初始化 PageLoader 的屏幕大小 - mPageLoader.prepareDisplay(mViewWidth, mViewHeight); - } - - return mPageLoader; - } - - public interface TouchListener { - boolean onTouch(); - - void center(); - - void prePage(); - - void nextPage(); - - void cancel(); - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/ReadSettingManager.kt b/app/src/main/java/com/novel/read/widget/page/ReadSettingManager.kt deleted file mode 100644 index b4f3adf..0000000 --- a/app/src/main/java/com/novel/read/widget/page/ReadSettingManager.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.novel.read.widget.page - - -import com.novel.read.utlis.DialogUtils -import com.novel.read.utlis.ScreenUtils -import com.novel.read.utlis.SpUtil - -class ReadSettingManager private constructor() { - - var brightness: Int - get() = SpUtil.getIntValue(SHARED_READ_BRIGHTNESS, 40) - set(progress) = SpUtil.setIntValue(SHARED_READ_BRIGHTNESS, progress) - - val isBrightnessAuto: Boolean - get() = SpUtil.getBooleanValue(SHARED_READ_IS_BRIGHTNESS_AUTO, false) - - var textSize: Int - get() = SpUtil.getIntValue(SHARED_READ_TEXT_SIZE, ScreenUtils.spToPx(16)) - set(textSize) = SpUtil.setIntValue(SHARED_READ_TEXT_SIZE, textSize) - - var isDefaultTextSize: Boolean - get() = SpUtil.getBooleanValue(SHARED_READ_IS_TEXT_DEFAULT, false) - set(isDefault) = SpUtil.setBooleanValue(SHARED_READ_IS_TEXT_DEFAULT, isDefault) - - var pageMode: PageMode - get() { - val mode = SpUtil.getIntValue(SHARED_READ_PAGE_MODE, PageMode.SIMULATION.ordinal) - return PageMode.values()[mode] - } - set(mode) = SpUtil.setIntValue(SHARED_READ_PAGE_MODE, mode.ordinal) - - var pageStyle: PageStyle - get() { - val style = SpUtil.getIntValue(SHARED_READ_BG, PageStyle.BG_0.ordinal) - return PageStyle.values()[style] - } - set(pageStyle) = SpUtil.setIntValue(SHARED_READ_BG, pageStyle.ordinal) - - var isNightMode: Boolean - get() = SpUtil.getBooleanValue(SHARED_READ_NIGHT_MODE, false) - set(isNight) = SpUtil.setBooleanValue(SHARED_READ_NIGHT_MODE, isNight) - - var isVolumeTurnPage: Boolean - get() = SpUtil.getBooleanValue(SHARED_READ_VOLUME_TURN_PAGE, false) - set(isTurn) = SpUtil.setBooleanValue(SHARED_READ_VOLUME_TURN_PAGE, isTurn) - - var isFullScreen: Boolean - get() = SpUtil.getBooleanValue(SHARED_READ_FULL_SCREEN, false) - set(isFullScreen) = SpUtil.setBooleanValue(SHARED_READ_FULL_SCREEN, isFullScreen) - - var convertType: Int - get() = SpUtil.getIntValue(SHARED_READ_CONVERT_TYPE, 1) - set(convertType) = SpUtil.setIntValue(SHARED_READ_CONVERT_TYPE, convertType) - - fun setAutoBrightness(isAuto: Boolean) { - SpUtil.setBooleanValue(SHARED_READ_IS_BRIGHTNESS_AUTO, isAuto) - } - - companion object { - /*************实在想不出什么好记的命名方式。。 */ - val READ_BG_DEFAULT = 0 - val READ_BG_1 = 1 - val READ_BG_2 = 2 - val READ_BG_3 = 3 - val READ_BG_4 = 4 - val NIGHT_MODE = 5 - - val SHARED_READ_BG = "shared_read_bg" - val SHARED_READ_BRIGHTNESS = "shared_read_brightness" - val SHARED_READ_IS_BRIGHTNESS_AUTO = "shared_read_is_brightness_auto" - val SHARED_READ_TEXT_SIZE = "shared_read_text_size" - val SHARED_READ_IS_TEXT_DEFAULT = "shared_read_text_default" - val SHARED_READ_PAGE_MODE = "shared_read_mode" - val SHARED_READ_NIGHT_MODE = "shared_night_mode" - val SHARED_READ_VOLUME_TURN_PAGE = "shared_read_volume_turn_page" - val SHARED_READ_FULL_SCREEN = "shared_read_full_screen" - val SHARED_READ_CONVERT_TYPE = "shared_read_convert_type" - -// @Volatile -// private var sInstance: ReadSettingManager? = null -// -// val instance: ReadSettingManager? -// get() { -// if (sInstance == null) { -// synchronized(ReadSettingManager::class.java) { -// if (sInstance == null) { -// sInstance = ReadSettingManager() -// } -// } -// } -// return sInstance -// } - @Volatile - private var instance: ReadSettingManager? = null - - @Synchronized - fun getInstance(): ReadSettingManager { - if (instance == null) { - instance = ReadSettingManager() - } - return instance as ReadSettingManager - } - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/TxtChapter.java b/app/src/main/java/com/novel/read/widget/page/TxtChapter.java deleted file mode 100644 index cb607aa..0000000 --- a/app/src/main/java/com/novel/read/widget/page/TxtChapter.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.novel.read.widget.page; - -/** - * Created by zlj - */ -public class TxtChapter { - - //章节所属的小说(网络) - String bookId; - //章节的链接(网络) - String link; - - //章节名(共用) - String title; - - //章节内容在文章中的起始位置(本地) - long start; - //章节内容在文章中的终止位置(本地) - long end; - String chapterId; - - - public String getBookId() { - return bookId; - } - - public void setBookId(String id) { - this.bookId = id; - } - - public String getLink() { - return link; - } - - public void setLink(String link) { - this.link = link; - } - - public String getTitle() { - return title == null ? "" : title; - } - - public void setTitle(String title) { - this.title = title; - } - - public long getStart() { - return start; - } - - public void setStart(long start) { - this.start = start; - } - - public long getEnd() { - return end; - } - - public void setEnd(long end) { - this.end = end; - } - - public String getChapterId() { - return chapterId == null ? "" : chapterId; - } - - public void setChapterId(String chapterId) { - this.chapterId = chapterId; - } - - - @Override - public String toString() { - return "TxtChapter{" + - "title='" + title + '\'' + - ", start=" + start + - ", end=" + end + - '}'; - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/TxtPage.java b/app/src/main/java/com/novel/read/widget/page/TxtPage.java deleted file mode 100644 index e1b93b2..0000000 --- a/app/src/main/java/com/novel/read/widget/page/TxtPage.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.novel.read.widget.page; - -import java.util.ArrayList; -import java.util.List; - -public class TxtPage { - int position; - String title; - int titleLines; //当前 lines 中为 title 的行数。 - List lines; - private String pic; - - public int getPosition() { - return position; - } - - public String getTitle() { - return title == null ? "" : title; - } - - - - public List getLines() { - if (lines == null) { - return new ArrayList<>(); - } - return lines; - } - - - public String getPic() { - return pic == null ? "" : pic; - } - - public void setPic(String pic) { - this.pic = pic; - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/Void.kt b/app/src/main/java/com/novel/read/widget/page/Void.kt deleted file mode 100644 index 9f07962..0000000 --- a/app/src/main/java/com/novel/read/widget/page/Void.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.novel.read.widget.page - -class Void diff --git a/app/src/main/java/com/novel/read/widget/page/anim/CoverPageAnim.kt b/app/src/main/java/com/novel/read/widget/page/anim/CoverPageAnim.kt deleted file mode 100644 index 4d41076..0000000 --- a/app/src/main/java/com/novel/read/widget/page/anim/CoverPageAnim.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.novel.read.widget.page.anim - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Rect -import android.graphics.drawable.GradientDrawable -import android.view.View -import com.novel.read.widget.page.PageAnimation - -/** - * Created by zlj - * 覆盖动画 - */ - -class CoverPageAnim(w: Int, h: Int, view: View, listener: OnPageChangeListener) : - HorizonPageAnim(w, h, view, listener) { - - private val mSrcRect: Rect = Rect(0, 0, mViewWidth, mViewHeight) - private val mDestRect: Rect = Rect(0, 0, mViewWidth, mViewHeight) - private val mBackShadowDrawableLR: GradientDrawable - - init { - val mBackShadowColors = intArrayOf(0x66000000, 0x00000000) - mBackShadowDrawableLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors - ) - mBackShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT - } - - override fun drawStatic(canvas: Canvas) { - if (isCancel) { - mNextBitmap = mCurBitmap.copy(Bitmap.Config.RGB_565, true) - canvas.drawBitmap(mCurBitmap, 0f, 0f, null) - } else { - canvas.drawBitmap(mNextBitmap, 0f, 0f, null) - } - } - - override fun drawMove(canvas: Canvas) { - - when (mDirection) { - PageAnimation.Direction.NEXT -> { - var dis = (mViewWidth - mStartX + mTouchX).toInt() - if (dis > mViewWidth) { - dis = mViewWidth - } - //计算bitmap截取的区域 - mSrcRect.left = mViewWidth - dis - //计算bitmap在canvas显示的区域 - mDestRect.right = dis - canvas.drawBitmap(mNextBitmap, 0f, 0f, null) - canvas.drawBitmap(mCurBitmap, mSrcRect, mDestRect, null) - addShadow(dis, canvas) - } - else -> { - mSrcRect.left = (mViewWidth - mTouchX).toInt() - mDestRect.right = mTouchX.toInt() - canvas.drawBitmap(mCurBitmap, 0f, 0f, null) - canvas.drawBitmap(mNextBitmap, mSrcRect, mDestRect, null) - addShadow(mTouchX.toInt(), canvas) - } - } - } - - //添加阴影 - fun addShadow(left: Int, canvas: Canvas) { - mBackShadowDrawableLR.setBounds(left, 0, left + 30, mScreenHeight) - mBackShadowDrawableLR.draw(canvas) - } - - override fun startAnim() { - super.startAnim() - var dx = 0 - when (mDirection) { - PageAnimation.Direction.NEXT -> if (isCancel) { - var dis = (mViewWidth - mStartX + mTouchX).toInt() - if (dis > mViewWidth) { - dis = mViewWidth - } - dx = mViewWidth - dis - } else { - dx = (-(mTouchX + (mViewWidth - mStartX))).toInt() - } - else -> if (isCancel) { - dx = (-mTouchX).toInt() - } else { - dx = (mViewWidth - mTouchX).toInt() - } - } - - //滑动速度保持一致 - val duration = 400 * Math.abs(dx) / mViewWidth - mScroller.startScroll(mTouchX.toInt(), 0, dx, 0, duration) - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/anim/HorizonPageAnim.kt b/app/src/main/java/com/novel/read/widget/page/anim/HorizonPageAnim.kt deleted file mode 100644 index 23300a8..0000000 --- a/app/src/main/java/com/novel/read/widget/page/anim/HorizonPageAnim.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.novel.read.widget.page.anim - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration - -import com.novel.read.widget.page.PageAnimation - -/** - * Created by zlj - * 横向动画 - */ - -abstract class HorizonPageAnim( - w: Int, h: Int, marginWidth: Int, marginHeight: Int, - view: View, listener: OnPageChangeListener -) : PageAnimation(w, h, marginWidth, marginHeight, view, listener) { - - protected var mCurBitmap: Bitmap - protected var mNextBitmap: Bitmap - //是否取消翻页 - protected var isCancel = false - - //可以使用 mLast代替 - private var mMoveX = 0 - private var mMoveY = 0 - //是否移动了 - private var isMove = false - //是否翻阅下一页。true表示翻到下一页,false表示上一页。 - private var isNext = false - - //是否没下一页或者上一页 - private var noNext = false - - constructor(w: Int, h: Int, view: View, listener: OnPageChangeListener) : this( - w, - h, - 0, - 0, - view, - listener - ) - - init { - //创建图片 - mCurBitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.RGB_565) - mNextBitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.RGB_565) - } - - /** - * 转换页面,在显示下一章的时候,必须首先调用此方法 - */ - fun changePage() { - val bitmap = mCurBitmap - mCurBitmap = mNextBitmap - mNextBitmap = bitmap - } - - abstract fun drawStatic(canvas: Canvas) - - abstract fun drawMove(canvas: Canvas) - - override fun onTouchEvent(event: MotionEvent): Boolean { - //获取点击位置 - val x = event.x.toInt() - val y = event.y.toInt() - //设置触摸点 - setTouchPoint(x.toFloat(), y.toFloat()) - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - //移动的点击位置 - mMoveX = 0 - mMoveY = 0 - //是否移动 - isMove = false - //是否存在下一章 - noNext = false - //是下一章还是前一章 - isNext = false - //是否正在执行动画 - isRunning = false - //取消 - isCancel = false - //设置起始位置的触摸点 - setStartPoint(x.toFloat(), y.toFloat()) - //如果存在动画则取消动画 - abortAnim() - } - MotionEvent.ACTION_MOVE -> { - val slop = ViewConfiguration.get(mView.context).scaledTouchSlop - //判断是否移动了 - if (!isMove) { - isMove = Math.abs(mStartX - x) > slop || Math.abs(mStartY - y) > slop - } - - if (isMove) { - //判断是否是准备移动的状态(将要移动但是还没有移动) - if (mMoveX == 0 && mMoveY == 0) { - //判断翻得是上一页还是下一页 - if (x - mStartX > 0) { - //上一页的参数配置 - isNext = false - val hasPrev = mListener.hasPrev() - direction = PageAnimation.Direction.PRE - //如果上一页不存在 - if (!hasPrev) { - noNext = true - return true - } - } else { - //进行下一页的配置 - isNext = true - //判断是否下一页存在 - val hasNext = mListener.hasNext() - //如果存在设置动画方向 - direction = PageAnimation.Direction.NEXT - - //如果不存在表示没有下一页了 - if (!hasNext) { - noNext = true - return true - } - } - } else { - //判断是否取消翻页 - if (isNext) { - isCancel = x - mMoveX > 0 - } else { - isCancel = x - mMoveX < 0 - } - } - - mMoveX = x - mMoveY = y - isRunning = true - mView.invalidate() - } - } - MotionEvent.ACTION_UP -> { - if (!isMove) { - isNext = x >= mScreenWidth / 2 - - if (isNext) { - //判断是否下一页存在 - val hasNext = mListener.hasNext() - //设置动画方向 - direction = PageAnimation.Direction.NEXT - if (!hasNext) { - return true - } - } else { - val hasPrev = mListener.hasPrev() - direction = PageAnimation.Direction.PRE - if (!hasPrev) { - return true - } - } - } - - // 是否取消翻页 - if (isCancel) { - mListener.pageCancel() - } - - // 开启翻页效果 - if (!noNext) { - startAnim() - mView.invalidate() - } - } - } - return true - } - - override fun draw(canvas: Canvas) { - if (isRunning) { - drawMove(canvas) - } else { - if (isCancel) { - mNextBitmap = mCurBitmap.copy(Bitmap.Config.RGB_565, true) - } - drawStatic(canvas) - } - } - - override fun scrollAnim() { - if (mScroller.computeScrollOffset()) { - val x = mScroller.currX - val y = mScroller.currY - - setTouchPoint(x.toFloat(), y.toFloat()) - - if (mScroller.finalX == x && mScroller.finalY == y) { - isRunning = false - } - mView.postInvalidate() - } - } - - override fun abortAnim() { - if (!mScroller.isFinished) { - mScroller.abortAnimation() - isRunning = false - setTouchPoint(mScroller.finalX.toFloat(), mScroller.finalY.toFloat()) - mView.postInvalidate() - } - } - - override fun getBgBitmap(): Bitmap { - return mNextBitmap - } - - override fun getNextBitmap(): Bitmap { - return mNextBitmap - } - - companion object { - private val TAG = "HorizonPageAnim" - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/anim/NonePageAnim.kt b/app/src/main/java/com/novel/read/widget/page/anim/NonePageAnim.kt deleted file mode 100644 index 6962fef..0000000 --- a/app/src/main/java/com/novel/read/widget/page/anim/NonePageAnim.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.novel.read.widget.page.anim - -import android.graphics.Canvas -import android.view.View -import com.novel.read.widget.page.PageAnimation - -/** - * Created by zlj - * 无 - */ - -class NonePageAnim(w: Int, h: Int, view: View, listener: OnPageChangeListener) : - HorizonPageAnim(w, h, view, listener) { - - override fun drawStatic(canvas: Canvas) { - if (isCancel) { - canvas.drawBitmap(mCurBitmap, 0f, 0f, null) - } else { - canvas.drawBitmap(mNextBitmap, 0f, 0f, null) - } - } - - override fun drawMove(canvas: Canvas) { - if (isCancel) { - canvas.drawBitmap(mCurBitmap, 0f, 0f, null) - } else { - canvas.drawBitmap(mNextBitmap, 0f, 0f, null) - } - } - - override fun startAnim() {} -} diff --git a/app/src/main/java/com/novel/read/widget/page/anim/ScrollPageAnim.kt b/app/src/main/java/com/novel/read/widget/page/anim/ScrollPageAnim.kt deleted file mode 100644 index 4893d7a..0000000 --- a/app/src/main/java/com/novel/read/widget/page/anim/ScrollPageAnim.kt +++ /dev/null @@ -1,412 +0,0 @@ -package com.novel.read.widget.page.anim - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Rect -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.View - -import com.novel.read.widget.page.PageAnimation - -import java.util.ArrayDeque -import java.util.ArrayList - -/** - * Created by zlj - * 原理:仿照ListView源码实现的上下滑动效果 - * 问题: - * 1. 向上翻页,重复的问题 (完成) - * 2. 滑动卡顿的问题。原因:由于绘制的数据过多造成的卡顿问题。 (主要是文字绘制需要的时长比较多) 解决办法:做文字缓冲 - * 3. 弱网环境下,显示的问题 - */ -class ScrollPageAnim( - w: Int, h: Int, marginWidth: Int, marginHeight: Int, - view: View, listener: OnPageChangeListener -) : PageAnimation(w, h, marginWidth, marginHeight, view, listener) { - private var mVelocity: VelocityTracker? = null - - // 整个Bitmap的背景显示 - private var mBgBitmap: Bitmap? = null - - // 下一个展示的图片 - private var mNextBitmap: Bitmap? = null - - // 被废弃的图片列表 - private var mScrapViews: ArrayDeque? = null - // 正在被利用的图片列表 - private val mActiveViews = ArrayList(2) - - // 是否处于刷新阶段 - private var isRefresh = true - - // 底部填充 - private var downIt: MutableIterator? = null - - private var upIt: MutableIterator? = null - - - private var tmpView: BitmapView? = null - - init { - // 创建两个BitmapView - initWidget() - } - - private fun initWidget() { - mBgBitmap = Bitmap.createBitmap(mScreenWidth, mScreenHeight, Bitmap.Config.RGB_565) - - mScrapViews = ArrayDeque(2) - for (i in 0..1) { - val view = BitmapView() - view.bitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.RGB_565) - view.srcRect = Rect(0, 0, mViewWidth, mViewHeight) - view.destRect = Rect(0, 0, mViewWidth, mViewHeight) - view.top = 0 - view.bottom = view.bitmap!!.height - - mScrapViews!!.push(view) - } - onLayout() - isRefresh = false - } - - // 修改布局,填充内容 - private fun onLayout() { - // 如果还没有开始加载,则从上到下进行绘制 - if (mActiveViews.size == 0) { - fillDown(0, 0) - mDirection = PageAnimation.Direction.NONE - } else { - val offset = (mTouchY - mLastY).toInt() - // 判断是下滑还是上拉 (下滑) - if (offset > 0) { - val topEdge = mActiveViews[0].top - fillUp(topEdge, offset) - } else { - // 底部的距离 = 当前底部的距离 + 滑动的距离 (因为上滑,得到的值肯定是负的) - val bottomEdge = mActiveViews[mActiveViews.size - 1].bottom - fillDown(bottomEdge, offset) - }// 上拉 - } - } - - /** - * 创建View填充底部空白部分 - * - * @param bottomEdge :当前最后一个View的底部,在整个屏幕上的位置,即相对于屏幕顶部的距离 - * @param offset :滑动的偏移量 - */ - private fun fillDown(bottomEdge: Int, offset: Int) { - - downIt = mActiveViews.iterator() - var view: BitmapView? - - // 进行删除 - while (downIt!!.hasNext()) { - view = downIt!!.next() - view.top = view.top + offset - view.bottom = view.bottom + offset - // 设置允许显示的范围 - view.destRect!!.top = view.top - view.destRect!!.bottom = view.bottom - - // 判断是否越界了 - if (view.bottom <= 0) { - // 添加到废弃的View中 - mScrapViews!!.add(view) - // 从Active中移除 - downIt!!.remove() - // 如果原先是从上加载,现在变成从下加载,则表示取消 - if (mDirection == PageAnimation.Direction.UP) { - mListener.pageCancel() - mDirection = PageAnimation.Direction.NONE - } - } - } - - // 滑动之后的最后一个 View 的距离屏幕顶部上的实际位置 - var realEdge = bottomEdge + offset - - // 进行填充 - while (realEdge < mViewHeight && mActiveViews.size < 2) { - // 从废弃的Views中获取一个 - view = mScrapViews!!.first - /* //擦除其Bitmap(重新创建会不会更好一点) - eraseBitmap(view.bitmap,view.bitmap.getWidth(),view.bitmap.getHeight(),0,0);*/ - if (view == null) return - - val cancelBitmap = mNextBitmap - mNextBitmap = view.bitmap - - if (!isRefresh) { - val hasNext = mListener.hasNext() //如果不成功则无法滑动 - - // 如果不存在next,则进行还原 - if (!hasNext) { - mNextBitmap = cancelBitmap - for (activeView in mActiveViews) { - activeView.top = 0 - activeView.bottom = mViewHeight - // 设置允许显示的范围 - activeView.destRect!!.top = activeView.top - activeView.destRect!!.bottom = activeView.bottom - } - abortAnim() - return - } - } - - // 如果加载成功,那么就将View从ScrapViews中移除 - mScrapViews!!.removeFirst() - // 添加到存活的Bitmap中 - mActiveViews.add(view) - mDirection = PageAnimation.Direction.DOWN - - // 设置Bitmap的范围 - view.top = realEdge - view.bottom = realEdge + view.bitmap!!.height - // 设置允许显示的范围 - view.destRect!!.top = view.top - view.destRect!!.bottom = view.bottom - - realEdge += view.bitmap!!.height - } - } - - /** - * 创建View填充顶部空白部分 - * - * @param topEdge : 当前第一个View的顶部,到屏幕顶部的距离 - * @param offset : 滑动的偏移量 - */ - private fun fillUp(topEdge: Int, offset: Int) { - // 首先进行布局的调整 - upIt = mActiveViews.iterator() - var view: BitmapView? - while (upIt!!.hasNext()) { - view = upIt!!.next() - view.top = view.top + offset - view.bottom = view.bottom + offset - //设置允许显示的范围 - view.destRect!!.top = view.top - view.destRect!!.bottom = view.bottom - - // 判断是否越界了 - if (view.top >= mViewHeight) { - // 添加到废弃的View中 - mScrapViews!!.add(view) - // 从Active中移除 - upIt!!.remove() - - // 如果原先是下,现在变成从上加载了,则表示取消加载 - - if (mDirection == PageAnimation.Direction.DOWN) { - mListener.pageCancel() - mDirection = PageAnimation.Direction.NONE - } - } - } - - // 滑动之后,第一个 View 的顶部距离屏幕顶部的实际位置。 - var realEdge = topEdge + offset - - // 对布局进行View填充 - while (realEdge > 0 && mActiveViews.size < 2) { - // 从废弃的Views中获取一个 - view = mScrapViews!!.first - if (view == null) return - - // 判断是否存在上一章节 - val cancelBitmap = mNextBitmap - mNextBitmap = view.bitmap - if (!isRefresh) { - val hasPrev = mListener.hasPrev() // 如果不成功则无法滑动 - // 如果不存在next,则进行还原 - if (!hasPrev) { - mNextBitmap = cancelBitmap - for (activeView in mActiveViews) { - activeView.top = 0 - activeView.bottom = mViewHeight - // 设置允许显示的范围 - activeView.destRect!!.top = activeView.top - activeView.destRect!!.bottom = activeView.bottom - } - abortAnim() - return - } - } - // 如果加载成功,那么就将View从ScrapViews中移除 - mScrapViews!!.removeFirst() - // 加入到存活的对象中 - mActiveViews.add(0, view) - mDirection = PageAnimation.Direction.UP - // 设置Bitmap的范围 - view.top = realEdge - view.bitmap!!.height - view.bottom = realEdge - - // 设置允许显示的范围 - view.destRect!!.top = view.top - view.destRect!!.bottom = view.bottom - realEdge -= view.bitmap!!.height - } - } - - /** - * 对Bitmap进行擦除 - * - * @param b - * @param width - * @param height - * @param paddingLeft - * @param paddingTop - */ - private fun eraseBitmap( - b: Bitmap, width: Int, height: Int, - paddingLeft: Int, paddingTop: Int - ) { - /* if (mInitBitmapPix == null) return; - b.setPixels(mInitBitmapPix, 0, width, paddingLeft, paddingTop, width, height);*/ - } - - /** - * 重置位移 - */ - fun resetBitmap() { - isRefresh = true - // 将所有的Active加入到Scrap中 - for (view in mActiveViews) { - mScrapViews!!.add(view) - } - // 清除所有的Active - mActiveViews.clear() - // 重新进行布局 - onLayout() - isRefresh = false - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - val x = event.x.toInt() - val y = event.y.toInt() - - // 初始化速度追踪器 - if (mVelocity == null) { - mVelocity = VelocityTracker.obtain() - } - - mVelocity!!.addMovement(event) - // 设置触碰点 - setTouchPoint(x.toFloat(), y.toFloat()) - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - isRunning = false - // 设置起始点 - setStartPoint(x.toFloat(), y.toFloat()) - // 停止动画 - abortAnim() - } - MotionEvent.ACTION_MOVE -> { - mVelocity!!.computeCurrentVelocity(VELOCITY_DURATION) - isRunning = true - // 进行刷新 - mView.postInvalidate() - } - MotionEvent.ACTION_UP -> { - isRunning = false - // 开启动画 - startAnim() - // 删除检测器 - mVelocity!!.recycle() - mVelocity = null - } - - MotionEvent.ACTION_CANCEL -> try { - mVelocity!!.recycle() // if velocityTracker won't be used should be recycled - mVelocity = null - } catch (e: Exception) { - e.printStackTrace() - } - - } - return true - } - - override fun draw(canvas: Canvas) { - //进行布局 - onLayout() - - //绘制背景 - canvas.drawBitmap(mBgBitmap!!, 0f, 0f, null) - //绘制内容 - canvas.save() - //移动位置 - canvas.translate(0f, mMarginHeight.toFloat()) - //裁剪显示区域 - canvas.clipRect(0, 0, mViewWidth, mViewHeight) - /* //设置背景透明 - canvas.drawColor(0x40);*/ - //绘制Bitmap - for (i in mActiveViews.indices) { - tmpView = mActiveViews[i] - canvas.drawBitmap(tmpView!!.bitmap!!, tmpView!!.srcRect, tmpView!!.destRect!!, null) - } - canvas.restore() - } - - @Synchronized - override fun startAnim() { - isRunning = true - mScroller.fling( - 0, - mTouchY.toInt(), - 0, - mVelocity!!.yVelocity.toInt(), - 0, - 0, - Integer.MAX_VALUE * -1, - Integer.MAX_VALUE - ) - } - - override fun scrollAnim() { - if (mScroller.computeScrollOffset()) { - val x = mScroller.currX - val y = mScroller.currY - setTouchPoint(x.toFloat(), y.toFloat()) - if (mScroller.finalX == x && mScroller.finalY == y) { - isRunning = false - } - mView.postInvalidate() - } - } - - override fun abortAnim() { - if (!mScroller.isFinished) { - mScroller.abortAnimation() - isRunning = false - } - } - - override fun getBgBitmap(): Bitmap? { - return mBgBitmap - } - - override fun getNextBitmap(): Bitmap? { - return mNextBitmap - } - - private class BitmapView { - internal var bitmap: Bitmap? = null - internal var srcRect: Rect? = null - internal var destRect: Rect? = null - internal var top: Int = 0 - internal var bottom: Int = 0 - } - - companion object { - private val TAG = "ScrollAnimation" - // 滑动追踪的时间 - private val VELOCITY_DURATION = 1000 - } -} diff --git a/app/src/main/java/com/novel/read/widget/page/anim/SimulationPageAnim.kt b/app/src/main/java/com/novel/read/widget/page/anim/SimulationPageAnim.kt deleted file mode 100644 index 68ba596..0000000 --- a/app/src/main/java/com/novel/read/widget/page/anim/SimulationPageAnim.kt +++ /dev/null @@ -1,664 +0,0 @@ -package com.novel.read.widget.page.anim - -import android.graphics.* -import android.graphics.drawable.GradientDrawable -import android.os.Build -import android.view.View -import com.novel.read.widget.page.PageAnimation -import kotlin.math.atan2 -import kotlin.math.hypot - -/** - * Created by zlj - * 仿真动画 - */ - -class SimulationPageAnim(w: Int, h: Int, view: View, listener: OnPageChangeListener) : - HorizonPageAnim(w, h, view, listener) { - - private var mCornerX = 1 // 拖拽点对应的页脚 - private var mCornerY = 1 - private val mPath0: Path = Path() - private val mPath1: Path = Path() - - private val mBezierStart1 = PointF() // 贝塞尔曲线起始点 - private val mBezierControl1 = PointF() // 贝塞尔曲线控制点 - private val mBeziervertex1 = PointF() // 贝塞尔曲线顶点 - private var mBezierEnd1 = PointF() // 贝塞尔曲线结束点 - - private val mBezierStart2 = PointF() // 另一条贝塞尔曲线 - private val mBezierControl2 = PointF() - private val mBeziervertex2 = PointF() - private var mBezierEnd2 = PointF() - - private var mMiddleX: Float = 0.toFloat() - private var mMiddleY: Float = 0.toFloat() - private var mDegrees: Float = 0.toFloat() - private var mTouchToCornerDis: Float = 0.toFloat() - private val mColorMatrixFilter: ColorMatrixColorFilter - private val mMatrix: Matrix - private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f) - - private var mIsRTandLB: Boolean = false // 是否属于右上左下 - private val mMaxLength: Float = - Math.hypot(mScreenWidth.toDouble(), mScreenHeight.toDouble()).toFloat() - private var mBackShadowColors: IntArray? = null// 背面颜色组 - private var mFrontShadowColors: IntArray? = null// 前面颜色组 - private var mBackShadowDrawableLR: GradientDrawable? = null // 有阴影的GradientDrawable - private var mBackShadowDrawableRL: GradientDrawable? = null - private var mFolderShadowDrawableLR: GradientDrawable? = null - private var mFolderShadowDrawableRL: GradientDrawable? = null - - private var mFrontShadowDrawableHBT: GradientDrawable? = null - private var mFrontShadowDrawableHTB: GradientDrawable? = null - private var mFrontShadowDrawableVLR: GradientDrawable? = null - private var mFrontShadowDrawableVRL: GradientDrawable? = null - - private val mPaint: Paint = Paint() - - init { - - mPaint.style = Paint.Style.FILL - - createDrawable() - - val cm = ColorMatrix()//设置颜色数组 - val array = floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f) - cm.set(array) - mColorMatrixFilter = ColorMatrixColorFilter(cm) - mMatrix = Matrix() - - mTouchX = 0.01f // 不让x,y为0,否则在点计算时会有问题 - mTouchY = 0.01f - } - - override fun drawMove(canvas: Canvas) { - when (mDirection) { - Direction.NEXT -> { - calcPoints() - drawCurrentPageArea(canvas, mCurBitmap, mPath0) - drawNextPageAreaAndShadow(canvas, mNextBitmap) - drawCurrentPageShadow(canvas) - drawCurrentBackArea(canvas, mCurBitmap) - } - else -> { - calcPoints() - drawCurrentPageArea(canvas, mNextBitmap, mPath0) - drawNextPageAreaAndShadow(canvas, mCurBitmap) - drawCurrentPageShadow(canvas) - drawCurrentBackArea(canvas, mNextBitmap) - } - } - } - - override fun drawStatic(canvas: Canvas) { - if (isCancel) { - mNextBitmap = mCurBitmap.copy(Bitmap.Config.RGB_565, true) - canvas.drawBitmap(mCurBitmap, 0f, 0f, null) - } else { - canvas.drawBitmap(mNextBitmap, 0f, 0f, null) - } - } - - override fun startAnim() { - super.startAnim() - var dx: Int - val dy: Int - // dx 水平方向滑动的距离,负值会使滚动向左滚动 - // dy 垂直方向滑动的距离,负值会使滚动向上滚动 - if (isCancel) { - - if (mCornerX > 0 && mDirection == Direction.NEXT) { - dx = (mScreenWidth - mTouchX).toInt() - } else { - dx = -mTouchX.toInt() - } - - if (mDirection != Direction.NEXT) { - dx = (-(mScreenWidth + mTouchX)).toInt() - } - - if (mCornerY > 0) { - dy = (mScreenHeight - mTouchY).toInt() - } else { - dy = -mTouchY.toInt() // 防止mTouchY最终变为0 - } - } else { - if (mCornerX > 0 && mDirection == Direction.NEXT) { - dx = -(mScreenWidth + mTouchX).toInt() - } else { - dx = (mScreenWidth - mTouchX + mScreenWidth).toInt() - } - if (mCornerY > 0) { - dy = (mScreenHeight - mTouchY).toInt() - } else { - dy = (1 - mTouchY).toInt() // 防止mTouchY最终变为0 - } - } - mScroller.startScroll(mTouchX.toInt(), mTouchY.toInt(), dx, dy, 400) - } - - override fun setDirection(direction: PageAnimation.Direction) { - super.setDirection(direction) - - when (direction) { - Direction.PRE -> - //上一页滑动不出现对角 - if (mStartX > mScreenWidth / 2) { - calcCornerXY(mStartX, mScreenHeight.toFloat()) - } else { - calcCornerXY(mScreenWidth - mStartX, mScreenHeight.toFloat()) - } - Direction.NEXT -> if (mScreenWidth / 2 > mStartX) { - calcCornerXY(mScreenWidth - mStartX, mStartY) - } - } - } - - override fun setStartPoint(x: Float, y: Float) { - super.setStartPoint(x, y) - calcCornerXY(x, y) - } - - override fun setTouchPoint(x: Float, y: Float) { - super.setTouchPoint(x, y) - //触摸y中间位置吧y变成屏幕高度 - if (mStartY > mScreenHeight / 3 && mStartY < mScreenHeight * 2 / 3 || mDirection == Direction.PRE) { - mTouchY = mScreenHeight.toFloat() - } - - if (mStartY > mScreenHeight / 3 && mStartY < mScreenHeight / 2 && mDirection == Direction.NEXT) { - mTouchY = 1f - } - } - - /** - * 创建阴影的GradientDrawable - */ - private fun createDrawable() { - val color = intArrayOf(0x333333, -0x4fcccccd) - mFolderShadowDrawableRL = GradientDrawable( - GradientDrawable.Orientation.RIGHT_LEFT, color - ) - mFolderShadowDrawableRL!!.gradientType = GradientDrawable.LINEAR_GRADIENT - - mFolderShadowDrawableLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, color - ) - mFolderShadowDrawableLR!!.gradientType = GradientDrawable.LINEAR_GRADIENT - - mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) - mBackShadowDrawableRL = GradientDrawable( - GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors - ) - mBackShadowDrawableRL!!.gradientType = GradientDrawable.LINEAR_GRADIENT - - mBackShadowDrawableLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors - ) - mBackShadowDrawableLR!!.gradientType = GradientDrawable.LINEAR_GRADIENT - - mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) - mFrontShadowDrawableVLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors - ) - mFrontShadowDrawableVLR!!.gradientType = GradientDrawable.LINEAR_GRADIENT - mFrontShadowDrawableVRL = GradientDrawable( - GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors - ) - mFrontShadowDrawableVRL!!.gradientType = GradientDrawable.LINEAR_GRADIENT - - mFrontShadowDrawableHTB = GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors - ) - mFrontShadowDrawableHTB!!.gradientType = GradientDrawable.LINEAR_GRADIENT - - mFrontShadowDrawableHBT = GradientDrawable( - GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors - ) - mFrontShadowDrawableHBT!!.gradientType = GradientDrawable.LINEAR_GRADIENT - } - - /** - * 是否能够拖动过去 - * - * @return - */ - fun canDragOver(): Boolean { - return if (mTouchToCornerDis > mScreenWidth / 10) true else false - } - - fun right(): Boolean { - return if (mCornerX > -4) false else true - } - - /** - * 绘制翻起页背面 - * - * @param canvas - * @param bitmap - */ - private fun drawCurrentBackArea(canvas: Canvas, bitmap: Bitmap) { - val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2 - val f1 = Math.abs(i - mBezierControl1.x) - val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2 - val f2 = Math.abs(i1 - mBezierControl2.y) - val f3 = Math.min(f1, f2) - mPath1.reset() - mPath1.moveTo(mBeziervertex2.x, mBeziervertex2.y) - mPath1.lineTo(mBeziervertex1.x, mBeziervertex1.y) - mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y) - mPath1.lineTo(mTouchX, mTouchY) - mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y) - mPath1.close() - val mFolderShadowDrawable: GradientDrawable? - val left: Int - val right: Int - if (mIsRTandLB) { - left = (mBezierStart1.x - 1).toInt() - right = (mBezierStart1.x + f3 + 1f).toInt() - mFolderShadowDrawable = mFolderShadowDrawableLR - } else { - left = (mBezierStart1.x - f3 - 1f).toInt() - right = (mBezierStart1.x + 1).toInt() - mFolderShadowDrawable = mFolderShadowDrawableRL - } - canvas.save() - try { - canvas.clipPath(mPath0) - if (Build.VERSION.SDK_INT >= 28) { - canvas.clipPath(mPath1) - } else { - canvas.clipPath(mPath1, Region.Op.INTERSECT) - } - } catch (e: Exception) { - } - - mPaint.colorFilter = mColorMatrixFilter - //对Bitmap进行取色 - val color = bitmap.getPixel(1, 1) - //获取对应的三色 - val red = color and 0xff0000 shr 16 - val green = color and 0x00ff00 shr 8 - val blue = color and 0x0000ff - //转换成含有透明度的颜色 - val tempColor = Color.argb(200, red, green, blue) - - - val dis = Math.hypot( - (mCornerX - mBezierControl1.x).toDouble(), - (mBezierControl2.y - mCornerY).toDouble() - ).toFloat() - val f8 = (mCornerX - mBezierControl1.x) / dis - val f9 = (mBezierControl2.y - mCornerY) / dis - mMatrixArray[0] = 1 - 2f * f9 * f9 - mMatrixArray[1] = 2f * f8 * f9 - mMatrixArray[3] = mMatrixArray[1] - mMatrixArray[4] = 1 - 2f * f8 * f8 - mMatrix.reset() - mMatrix.setValues(mMatrixArray) - mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y) - mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y) - canvas.drawBitmap(bitmap, mMatrix, mPaint) - //背景叠加 - canvas.drawColor(tempColor) - - mPaint.colorFilter = null - - canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) - mFolderShadowDrawable!!.setBounds( - left, mBezierStart1.y.toInt(), right, - (mBezierStart1.y + mMaxLength).toInt() - ) - mFolderShadowDrawable.draw(canvas) - canvas.restore() - } - - /** - * 绘制翻起页的阴影 - * - * @param canvas - */ - fun drawCurrentPageShadow(canvas: Canvas) { - val degree: Double - if (mIsRTandLB) { - degree = Math.PI / 4 - Math.atan2( - (mBezierControl1.y - mTouchY).toDouble(), - (mTouchX - mBezierControl1.x).toDouble() - ) - } else { - degree = Math.PI / 4 - Math.atan2( - (mTouchY - mBezierControl1.y).toDouble(), - (mTouchX - mBezierControl1.x).toDouble() - ) - } - // 翻起页阴影顶点与touch点的距离 - val d1 = 25.toFloat().toDouble() * 1.414 * Math.cos(degree) - val d2 = 25.toFloat().toDouble() * 1.414 * Math.sin(degree) - val x = (mTouchX + d1).toFloat() - val y: Float - if (mIsRTandLB) { - y = (mTouchY + d2).toFloat() - } else { - y = (mTouchY - d2).toFloat() - } - mPath1.reset() - mPath1.moveTo(x, y) - mPath1.lineTo(mTouchX, mTouchY) - mPath1.lineTo(mBezierControl1.x, mBezierControl1.y) - mPath1.lineTo(mBezierStart1.x, mBezierStart1.y) - mPath1.close() - var rotateDegrees: Float - canvas.save() - try { - - if (Build.VERSION.SDK_INT >= 28) { - canvas.clipOutPath(mPath0) - canvas.clipPath(mPath1) - } else { - canvas.clipPath(mPath0, Region.Op.XOR) - canvas.clipPath(mPath1, Region.Op.INTERSECT) - - } - - } catch (e: Exception) { - // TODO: handle exception - } - - var leftx: Int - var rightx: Int - var mCurrentPageShadow: GradientDrawable? - if (mIsRTandLB) { - leftx = mBezierControl1.x.toInt() - rightx = mBezierControl1.x.toInt() + 25 - mCurrentPageShadow = mFrontShadowDrawableVLR - } else { - leftx = (mBezierControl1.x - 25).toInt() - rightx = mBezierControl1.x.toInt() + 1 - mCurrentPageShadow = mFrontShadowDrawableVRL - } - - rotateDegrees = Math.toDegrees( - Math.atan2( - (mTouchX - mBezierControl1.x).toDouble(), - (mBezierControl1.y - mTouchY).toDouble() - ) - ).toFloat() - canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) - mCurrentPageShadow!!.setBounds( - leftx, - (mBezierControl1.y - mMaxLength).toInt(), rightx, - mBezierControl1.y.toInt() - ) - mCurrentPageShadow.draw(canvas) - canvas.restore() - - mPath1.reset() - mPath1.moveTo(x, y) - mPath1.lineTo(mTouchX, mTouchY) - mPath1.lineTo(mBezierControl2.x, mBezierControl2.y) - mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) - mPath1.close() - canvas.save() - try { - if (Build.VERSION.SDK_INT >= 28) { - canvas.clipOutPath(mPath0) - canvas.clipPath(mPath1) - } else { - canvas.clipPath(mPath0, Region.Op.XOR) - canvas.clipPath(mPath1, Region.Op.INTERSECT) - } - } catch (e: Exception) { - } - - if (mIsRTandLB) { - leftx = mBezierControl2.y.toInt() - rightx = (mBezierControl2.y + 25).toInt() - mCurrentPageShadow = mFrontShadowDrawableHTB - } else { - leftx = (mBezierControl2.y - 25).toInt() - rightx = (mBezierControl2.y + 1).toInt() - mCurrentPageShadow = mFrontShadowDrawableHBT - } - rotateDegrees = Math.toDegrees( - atan2( - (mBezierControl2.y - mTouchY).toDouble(), - (mBezierControl2.x - mTouchX).toDouble() - ) - ).toFloat() - canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) - val temp: Float - if (mBezierControl2.y < 0) - temp = mBezierControl2.y - mScreenHeight - else - temp = mBezierControl2.y - - val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt() - if (hmg > mMaxLength) - mCurrentPageShadow!! - .setBounds( - (mBezierControl2.x - 25).toInt() - hmg, leftx, - (mBezierControl2.x + mMaxLength).toInt() - hmg, - rightx - ) - else - mCurrentPageShadow!!.setBounds( - (mBezierControl2.x - mMaxLength).toInt(), leftx, - mBezierControl2.x.toInt(), rightx - ) - - mCurrentPageShadow.draw(canvas) - canvas.restore() - } - - private fun drawNextPageAreaAndShadow(canvas: Canvas, bitmap: Bitmap) { - mPath1.reset() - mPath1.moveTo(mBezierStart1.x, mBezierStart1.y) - mPath1.lineTo(mBeziervertex1.x, mBeziervertex1.y) - mPath1.lineTo(mBeziervertex2.x, mBeziervertex2.y) - mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) - mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) - mPath1.close() - - mDegrees = Math.toDegrees( - Math.atan2( - (mBezierControl1.x - mCornerX).toDouble(), - (mBezierControl2.y - mCornerY).toDouble() - ) - ).toFloat() - val leftx: Int - val rightx: Int - val mBackShadowDrawable: GradientDrawable? - if (mIsRTandLB) { //左下及右上 - leftx = mBezierStart1.x.toInt() - rightx = (mBezierStart1.x + mTouchToCornerDis / 4).toInt() - mBackShadowDrawable = mBackShadowDrawableLR - } else { - leftx = (mBezierStart1.x - mTouchToCornerDis / 4).toInt() - rightx = mBezierStart1.x.toInt() - mBackShadowDrawable = mBackShadowDrawableRL - } - canvas.save() - try { - - if (Build.VERSION.SDK_INT >= 28) { - canvas.clipPath(mPath0) - canvas.clipPath(mPath1) - } else { - canvas.clipPath(mPath0) - canvas.clipPath(mPath1, Region.Op.INTERSECT) - } - } catch (e: Exception) { - } - - - canvas.drawBitmap(bitmap, 0f, 0f, null) - canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) - mBackShadowDrawable!!.setBounds( - leftx, mBezierStart1.y.toInt(), rightx, - (mMaxLength + mBezierStart1.y).toInt() - )//左上及右下角的xy坐标值,构成一个矩形 - mBackShadowDrawable.draw(canvas) - canvas.restore() - } - - private fun drawCurrentPageArea(canvas: Canvas, bitmap: Bitmap, path: Path) { - mPath0.reset() - mPath0.moveTo(mBezierStart1.x, mBezierStart1.y) - mPath0.quadTo( - mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, - mBezierEnd1.y - ) - mPath0.lineTo(mTouchX, mTouchY) - mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y) - mPath0.quadTo( - mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, - mBezierStart2.y - ) - mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) - mPath0.close() - - canvas.save() - // if(Build.VERSION.SDK_INT >= 28){ - // canvas.clipOutPath(path); - // }else { - // canvas.clipPath(path, Region.Op.XOR); - // } - canvas.drawBitmap(bitmap, 0f, 0f, null) - try { - canvas.restore() - } catch (e: Exception) { - - } - - } - - /** - * 计算拖拽点对应的拖拽脚 - * - * @param x - * @param y - */ - fun calcCornerXY(x: Float, y: Float) { - if (x <= mScreenWidth / 2) { - mCornerX = 0 - } else { - mCornerX = mScreenWidth - } - if (y <= mScreenHeight / 2) { - mCornerY = 0 - } else { - mCornerY = mScreenHeight - } - - if (mCornerX == 0 && mCornerY == mScreenHeight || mCornerX == mScreenWidth && mCornerY == 0) { - mIsRTandLB = true - } else { - mIsRTandLB = false - } - - } - - private fun calcPoints() { - mMiddleX = (mTouchX + mCornerX) / 2 - mMiddleY = (mTouchY + mCornerY) / 2 - mBezierControl1.x = - mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) - mBezierControl1.y = mCornerY.toFloat() - mBezierControl2.x = mCornerX.toFloat() - - val f4 = mCornerY - mMiddleY - if (f4 == 0f) { - mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f - - } else { - mBezierControl2.y = - mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) - } - mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 - mBezierStart1.y = mCornerY.toFloat() - - // 当mBezierStart1.x < 0或者mBezierStart1.x > 480时 - // 如果继续翻页,会出现BUG故在此限制 - if (mTouchX > 0 && mTouchX < mScreenWidth) { - if (mBezierStart1.x < 0 || mBezierStart1.x > mScreenWidth) { - if (mBezierStart1.x < 0) - mBezierStart1.x = mScreenWidth - mBezierStart1.x - - val f1 = Math.abs(mCornerX - mTouchX) - val f2 = mScreenWidth * f1 / mBezierStart1.x - mTouchX = Math.abs(mCornerX - f2) - - val f3 = Math.abs(mCornerX - mTouchX) * Math.abs(mCornerY - mTouchY) / f1 - mTouchY = Math.abs(mCornerY - f3) - - mMiddleX = (mTouchX + mCornerX) / 2 - mMiddleY = (mTouchY + mCornerY) / 2 - - mBezierControl1.x = - mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) - mBezierControl1.y = mCornerY.toFloat() - - mBezierControl2.x = mCornerX.toFloat() - - val f5 = mCornerY - mMiddleY - if (f5 == 0f) { - mBezierControl2.y = - mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f - } else { - mBezierControl2.y = - mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) - } - - mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 - } - } - mBezierStart2.x = mCornerX.toFloat() - mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2 - - mTouchToCornerDis = Math.hypot( - (mTouchX - mCornerX).toDouble(), - (mTouchY - mCornerY).toDouble() - ).toFloat() - - mBezierEnd1 = getCross( - PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1, - mBezierStart2 - ) - mBezierEnd2 = getCross( - PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1, - mBezierStart2 - ) - - mBeziervertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 - mBeziervertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 - mBeziervertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 - mBeziervertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4 - } - - /** - * 求解直线P1P2和直线P3P4的交点坐标 - * - * @param P1 - * @param P2 - * @param P3 - * @param P4 - * @return - */ - fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF { - val CrossP = PointF() - // 二元函数通式: y=ax+b - val a1 = (P2.y - P1.y) / (P2.x - P1.x) - val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x) - - val a2 = (P4.y - P3.y) / (P4.x - P3.x) - val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x) - CrossP.x = (b2 - b1) / (a1 - a2) - CrossP.y = a1 * CrossP.x + b1 - return CrossP - } - - companion object { - private val TAG = "SimulationPageAnim" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/novel/read/widget/page/anim/SlidePageAnim.kt b/app/src/main/java/com/novel/read/widget/page/anim/SlidePageAnim.kt deleted file mode 100644 index 355f3e7..0000000 --- a/app/src/main/java/com/novel/read/widget/page/anim/SlidePageAnim.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.novel.read.widget.page.anim - -import android.graphics.Canvas -import android.graphics.Rect -import android.view.View -import com.novel.read.widget.page.PageAnimation - -/** - * Created by zlj - */ - -class SlidePageAnim(w: Int, h: Int, view: View, listener: OnPageChangeListener) : - HorizonPageAnim(w, h, view, listener) { - - private val mSrcRect: Rect = Rect(0, 0, mViewWidth, mViewHeight) - private val mDestRect: Rect = Rect(0, 0, mViewWidth, mViewHeight) - private val mNextSrcRect: Rect = Rect(0, 0, mViewWidth, mViewHeight) - private val mNextDestRect: Rect = Rect(0, 0, mViewWidth, mViewHeight) - - override fun drawStatic(canvas: Canvas) { - if (isCancel) { - canvas.drawBitmap(mCurBitmap, 0f, 0f, null) - } else { - canvas.drawBitmap(mNextBitmap, 0f, 0f, null) - } - } - - override fun drawMove(canvas: Canvas) { - var dis: Int - when (mDirection) { - Direction.NEXT -> { - //左半边的剩余区域 - dis = (mScreenWidth - mStartX + mTouchX).toInt() - if (dis > mScreenWidth) { - dis = mScreenWidth - } - //计算bitmap截取的区域 - mSrcRect.left = mScreenWidth - dis - //计算bitmap在canvas显示的区域 - mDestRect.right = dis - //计算下一页截取的区域 - mNextSrcRect.right = mScreenWidth - dis - //计算下一页在canvas显示的区域 - mNextDestRect.left = dis - - canvas.drawBitmap(mNextBitmap, mNextSrcRect, mNextDestRect, null) - canvas.drawBitmap(mCurBitmap, mSrcRect, mDestRect, null) - } - else -> { - dis = (mTouchX - mStartX).toInt() - if (dis < 0) { - dis = 0 - mStartX = mTouchX - } - mSrcRect.left = mScreenWidth - dis - mDestRect.right = dis - - //计算下一页截取的区域 - mNextSrcRect.right = mScreenWidth - dis - //计算下一页在canvas显示的区域 - mNextDestRect.left = dis - - canvas.drawBitmap(mCurBitmap, mNextSrcRect, mNextDestRect, null) - canvas.drawBitmap(mNextBitmap, mSrcRect, mDestRect, null) - } - } - } - - override fun startAnim() { - super.startAnim() - val dx: Int - when (mDirection) { - Direction.NEXT -> if (isCancel) { - var dis = (mScreenWidth - mStartX + mTouchX).toInt() - if (dis > mScreenWidth) { - dis = mScreenWidth - } - dx = mScreenWidth - dis - } else { - dx = (-(mTouchX + (mScreenWidth - mStartX))).toInt() - } - else -> if (isCancel) { - dx = (-Math.abs(mTouchX - mStartX)).toInt() - } else { - dx = (mScreenWidth - (mTouchX - mStartX)).toInt() - } - } - //滑动速度保持一致 - val duration = 400 * Math.abs(dx) / mScreenWidth - mScroller.startScroll(mTouchX.toInt(), 0, dx, 0, duration) - } -} diff --git a/app/src/main/res/anim/anim_readbook_bottom_in.xml b/app/src/main/res/anim/anim_readbook_bottom_in.xml new file mode 100644 index 0000000..48fe0c0 --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_bottom_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_bottom_out.xml b/app/src/main/res/anim/anim_readbook_bottom_out.xml new file mode 100644 index 0000000..2e91beb --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_bottom_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_top_in.xml b/app/src/main/res/anim/anim_readbook_top_in.xml new file mode 100644 index 0000000..e36360b --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_top_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_top_out.xml b/app/src/main/res/anim/anim_readbook_top_out.xml new file mode 100644 index 0000000..0b8acbc --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_top_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_bottom_in.xml b/app/src/main/res/anim/slide_bottom_in.xml deleted file mode 100644 index 1dd6c09..0000000 --- a/app/src/main/res/anim/slide_bottom_in.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_bottom_out.xml b/app/src/main/res/anim/slide_bottom_out.xml deleted file mode 100644 index d333fb7..0000000 --- a/app/src/main/res/anim/slide_bottom_out.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left_in.xml b/app/src/main/res/anim/slide_left_in.xml deleted file mode 100644 index 842a616..0000000 --- a/app/src/main/res/anim/slide_left_in.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left_out.xml b/app/src/main/res/anim/slide_left_out.xml deleted file mode 100644 index d9c3bd9..0000000 --- a/app/src/main/res/anim/slide_left_out.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right_in.xml b/app/src/main/res/anim/slide_right_in.xml deleted file mode 100644 index c3ccec1..0000000 --- a/app/src/main/res/anim/slide_right_in.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right_out.xml b/app/src/main/res/anim/slide_right_out.xml deleted file mode 100644 index 77ee771..0000000 --- a/app/src/main/res/anim/slide_right_out.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_top_in.xml b/app/src/main/res/anim/slide_top_in.xml deleted file mode 100644 index 177e700..0000000 --- a/app/src/main/res/anim/slide_top_in.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_top_out.xml b/app/src/main/res/anim/slide_top_out.xml deleted file mode 100644 index 7b51c98..0000000 --- a/app/src/main/res/anim/slide_top_out.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-night-xxhdpi/ic_back.png b/app/src/main/res/drawable-night-xxhdpi/ic_back.png deleted file mode 100644 index 8ba8d4a..0000000 Binary files a/app/src/main/res/drawable-night-xxhdpi/ic_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-night-xxhdpi/ic_edit.png b/app/src/main/res/drawable-night-xxhdpi/ic_edit.png deleted file mode 100644 index 73765cc..0000000 Binary files a/app/src/main/res/drawable-night-xxhdpi/ic_edit.png and /dev/null differ diff --git a/app/src/main/res/drawable-night-xxhdpi/ic_menu_mode_night_normal.png b/app/src/main/res/drawable-night-xxhdpi/ic_menu_mode_night_normal.png deleted file mode 100644 index 3847e08..0000000 Binary files a/app/src/main/res/drawable-night-xxhdpi/ic_menu_mode_night_normal.png and /dev/null differ diff --git a/app/src/main/res/drawable-night-xxhdpi/ic_search.png b/app/src/main/res/drawable-night-xxhdpi/ic_search.png deleted file mode 100644 index 70f6db2..0000000 Binary files a/app/src/main/res/drawable-night-xxhdpi/ic_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-night-xxhdpi/ic_theme.png b/app/src/main/res/drawable-night-xxhdpi/ic_theme.png deleted file mode 100644 index c7c2fda..0000000 Binary files a/app/src/main/res/drawable-night-xxhdpi/ic_theme.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/cover_default.png b/app/src/main/res/drawable-xxhdpi/cover_default.png deleted file mode 100644 index f81b9d7..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/cover_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_back.png b/app/src/main/res/drawable-xxhdpi/ic_back.png deleted file mode 100644 index e15f8e7..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_add.png b/app/src/main/res/drawable-xxhdpi/ic_book_add.png deleted file mode 100644 index 0b0962a..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_book_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_detail.png b/app/src/main/res/drawable-xxhdpi/ic_book_detail.png deleted file mode 100644 index 77cb3e2..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_book_detail.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_end.png b/app/src/main/res/drawable-xxhdpi/ic_book_end.png new file mode 100644 index 0000000..8f637ec Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_book_end.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_n.png b/app/src/main/res/drawable-xxhdpi/ic_book_n.png deleted file mode 100644 index fc50106..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_book_n.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_p.png b/app/src/main/res/drawable-xxhdpi/ic_book_p.png deleted file mode 100644 index 9277e01..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_book_p.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_book_type.png b/app/src/main/res/drawable-xxhdpi/ic_book_type.png new file mode 100644 index 0000000..01cb78c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_book_type.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_cache.png b/app/src/main/res/drawable-xxhdpi/ic_cache.png deleted file mode 100644 index 0acfe4d..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_cache.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxhdpi/ic_cancel.png deleted file mode 100644 index fbebab5..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_cancel.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_checked.png b/app/src/main/res/drawable-xxhdpi/ic_checked.png deleted file mode 100644 index 881a033..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_checked.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_click.png b/app/src/main/res/drawable-xxhdpi/ic_click.png new file mode 100644 index 0000000..97cfa50 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_click.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_contents.png b/app/src/main/res/drawable-xxhdpi/ic_contents.png deleted file mode 100644 index e260c20..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_contents.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_daily.png b/app/src/main/res/drawable-xxhdpi/ic_daily.png new file mode 100644 index 0000000..8b89b05 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_daily.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_default.png b/app/src/main/res/drawable-xxhdpi/ic_default.png deleted file mode 100644 index 6db2242..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit.png b/app/src/main/res/drawable-xxhdpi/ic_edit.png deleted file mode 100644 index 677adbd..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_edit.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit_recommend.png b/app/src/main/res/drawable-xxhdpi/ic_edit_recommend.png deleted file mode 100644 index 6edc4c2..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_edit_recommend.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_end.png b/app/src/main/res/drawable-xxhdpi/ic_end.png new file mode 100644 index 0000000..e04647d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_end.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_font_add.png b/app/src/main/res/drawable-xxhdpi/ic_font_add.png deleted file mode 100644 index bf25018..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_font_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_font_min.png b/app/src/main/res/drawable-xxhdpi/ic_font_min.png deleted file mode 100644 index 4374218..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_font_min.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_guide.jpg b/app/src/main/res/drawable-xxhdpi/ic_guide.jpg deleted file mode 100644 index b453d0b..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_guide.jpg and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_history.png b/app/src/main/res/drawable-xxhdpi/ic_history.png deleted file mode 100644 index a371122..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_history.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hot.png b/app/src/main/res/drawable-xxhdpi/ic_hot.png index fc502e5..0b697f9 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_hot.png and b/app/src/main/res/drawable-xxhdpi/ic_hot.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hot_search.png b/app/src/main/res/drawable-xxhdpi/ic_hot_search.png new file mode 100644 index 0000000..98ce3c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_hot_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_human.png b/app/src/main/res/drawable-xxhdpi/ic_human.png deleted file mode 100644 index 9cc67ce..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_human.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_item_category_activated.png b/app/src/main/res/drawable-xxhdpi/ic_item_category_activated.png deleted file mode 100644 index 8712b28..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_item_category_activated.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_item_category_download.png b/app/src/main/res/drawable-xxhdpi/ic_item_category_download.png deleted file mode 100644 index bb61a3c..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_item_category_download.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_item_category_normal.png b/app/src/main/res/drawable-xxhdpi/ic_item_category_normal.png deleted file mode 100644 index b4e9b49..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_item_category_normal.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_label.png b/app/src/main/res/drawable-xxhdpi/ic_label.png deleted file mode 100644 index c56fbbd..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_label.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_light.png b/app/src/main/res/drawable-xxhdpi/ic_light.png deleted file mode 100644 index 4bf7929..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_listen.png b/app/src/main/res/drawable-xxhdpi/ic_listen.png deleted file mode 100644 index 524b6e2..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_listen.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_love_look.png b/app/src/main/res/drawable-xxhdpi/ic_love_look.png deleted file mode 100644 index 5d34cf8..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_love_look.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_lovely.png b/app/src/main/res/drawable-xxhdpi/ic_lovely.png deleted file mode 100644 index 38e9db6..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_lovely.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_add_mark.png b/app/src/main/res/drawable-xxhdpi/ic_menu_add_mark.png deleted file mode 100644 index 157c022..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_menu_add_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_clear.png b/app/src/main/res/drawable-xxhdpi/ic_menu_clear.png deleted file mode 100644 index e88abb1..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_menu_clear.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_mode_night_normal.png b/app/src/main/res/drawable-xxhdpi/ic_menu_mode_night_normal.png deleted file mode 100644 index c7c2fda..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_menu_mode_night_normal.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_n.png b/app/src/main/res/drawable-xxhdpi/ic_more_n.png deleted file mode 100644 index 61085bb..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_more_n.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_p.png b/app/src/main/res/drawable-xxhdpi/ic_more_p.png deleted file mode 100644 index b184dba..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_more_p.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_no_select.png b/app/src/main/res/drawable-xxhdpi/ic_no_select.png deleted file mode 100644 index 9034e13..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_no_select.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rank.png b/app/src/main/res/drawable-xxhdpi/ic_rank.png new file mode 100644 index 0000000..360efb1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_rank.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_read_menu_moring.png b/app/src/main/res/drawable-xxhdpi/ic_read_menu_moring.png deleted file mode 100644 index 3847e08..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_read_menu_moring.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_read_menu_night.png b/app/src/main/res/drawable-xxhdpi/ic_read_menu_night.png deleted file mode 100644 index c7c2fda..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_read_menu_night.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_read_setting.png b/app/src/main/res/drawable-xxhdpi/ic_read_setting.png deleted file mode 100644 index 9ccd45b..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_read_setting.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_recent_update.png b/app/src/main/res/drawable-xxhdpi/ic_recent_update.png deleted file mode 100644 index 7363b3e..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_recent_update.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_recommend.png b/app/src/main/res/drawable-xxhdpi/ic_recommend.png new file mode 100644 index 0000000..3892399 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_recommend.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_recommend_n.png b/app/src/main/res/drawable-xxhdpi/ic_recommend_n.png deleted file mode 100644 index ed66b41..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_recommend_n.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_recommend_p.png b/app/src/main/res/drawable-xxhdpi/ic_recommend_p.png deleted file mode 100644 index 7ca4fa2..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_recommend_p.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search.png b/app/src/main/res/drawable-xxhdpi/ic_search.png deleted file mode 100644 index 7eee4f1..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_search.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_select.png b/app/src/main/res/drawable-xxhdpi/ic_select.png deleted file mode 100644 index b87f2e5..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_select.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stack_n.png b/app/src/main/res/drawable-xxhdpi/ic_stack_n.png deleted file mode 100644 index ee07bd2..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_stack_n.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stack_p.png b/app/src/main/res/drawable-xxhdpi/ic_stack_p.png deleted file mode 100644 index f44619f..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_stack_p.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_star.png b/app/src/main/res/drawable-xxhdpi/ic_star.png deleted file mode 100644 index a541518..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_theme.png b/app/src/main/res/drawable-xxhdpi/ic_theme.png deleted file mode 100644 index 3847e08..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_theme.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_type_default.png b/app/src/main/res/drawable-xxhdpi/ic_type_default.png deleted file mode 100644 index d06bf8c..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_type_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/seekbar_thumb_normal.png b/app/src/main/res/drawable-xxhdpi/seekbar_thumb_normal.png deleted file mode 100644 index d043cb9..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/seekbar_thumb_normal.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/seekbar_thumb_selected.png b/app/src/main/res/drawable-xxhdpi/seekbar_thumb_selected.png deleted file mode 100644 index 16106bb..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/seekbar_thumb_selected.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/theme_leather_bg.jpg b/app/src/main/res/drawable-xxhdpi/theme_leather_bg.jpg deleted file mode 100644 index 1aa0460..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/theme_leather_bg.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/bg_sex.xml b/app/src/main/res/drawable/bg_banner.xml similarity index 53% rename from app/src/main/res/drawable/bg_sex.xml rename to app/src/main/res/drawable/bg_banner.xml index 197ae45..6033be6 100644 --- a/app/src/main/res/drawable/bg_sex.xml +++ b/app/src/main/res/drawable/bg_banner.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_blue_touch.xml b/app/src/main/res/drawable/bg_blue_touch.xml deleted file mode 100644 index 4071b04..0000000 --- a/app/src/main/res/drawable/bg_blue_touch.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_category.xml b/app/src/main/res/drawable/bg_category.xml new file mode 100644 index 0000000..388dd6f --- /dev/null +++ b/app/src/main/res/drawable/bg_category.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/listen_select.xml b/app/src/main/res/drawable/bg_corner_trans.xml similarity index 52% rename from app/src/main/res/drawable/listen_select.xml rename to app/src/main/res/drawable/bg_corner_trans.xml index 18fba8a..3ccb029 100644 --- a/app/src/main/res/drawable/listen_select.xml +++ b/app/src/main/res/drawable/bg_corner_trans.xml @@ -1,7 +1,5 @@ - - - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_corner_white.xml b/app/src/main/res/drawable/bg_corner_white.xml index 7d542dc..56c3fd3 100644 --- a/app/src/main/res/drawable/bg_corner_white.xml +++ b/app/src/main/res/drawable/bg_corner_white.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edit.xml b/app/src/main/res/drawable/bg_edit.xml new file mode 100644 index 0000000..9653cca --- /dev/null +++ b/app/src/main/res/drawable/bg_edit.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_feedback.xml b/app/src/main/res/drawable/bg_feedback.xml new file mode 100644 index 0000000..ebfc279 --- /dev/null +++ b/app/src/main/res/drawable/bg_feedback.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_coner_line.xml b/app/src/main/res/drawable/bg_google_login.xml similarity index 64% rename from app/src/main/res/drawable/bg_coner_line.xml rename to app/src/main/res/drawable/bg_google_login.xml index 856faa6..91e683c 100644 --- a/app/src/main/res/drawable/bg_coner_line.xml +++ b/app/src/main/res/drawable/bg_google_login.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_listen.xml b/app/src/main/res/drawable/bg_listen.xml deleted file mode 100644 index 14c96cc..0000000 --- a/app/src/main/res/drawable/bg_listen.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_login.xml b/app/src/main/res/drawable/bg_login.xml new file mode 100644 index 0000000..a16e689 --- /dev/null +++ b/app/src/main/res/drawable/bg_login.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_login_n.xml b/app/src/main/res/drawable/bg_login_n.xml new file mode 100644 index 0000000..39b5e82 --- /dev/null +++ b/app/src/main/res/drawable/bg_login_n.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_login_p.xml b/app/src/main/res/drawable/bg_login_p.xml new file mode 100644 index 0000000..63ec88b --- /dev/null +++ b/app/src/main/res/drawable/bg_login_p.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_mine_buy.xml b/app/src/main/res/drawable/bg_mine_buy.xml new file mode 100644 index 0000000..c539213 --- /dev/null +++ b/app/src/main/res/drawable/bg_mine_buy.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_mine_buy_n.xml b/app/src/main/res/drawable/bg_mine_buy_n.xml new file mode 100644 index 0000000..6870496 --- /dev/null +++ b/app/src/main/res/drawable/bg_mine_buy_n.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_mine_buy_p.xml b/app/src/main/res/drawable/bg_mine_buy_p.xml new file mode 100644 index 0000000..4e9b5f1 --- /dev/null +++ b/app/src/main/res/drawable/bg_mine_buy_p.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_mine_vip.xml b/app/src/main/res/drawable/bg_mine_vip.xml new file mode 100644 index 0000000..13120f6 --- /dev/null +++ b/app/src/main/res/drawable/bg_mine_vip.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_popup_menu.xml b/app/src/main/res/drawable/bg_popup_menu.xml new file mode 100644 index 0000000..b51849b --- /dev/null +++ b/app/src/main/res/drawable/bg_popup_menu.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_prefs_color.xml b/app/src/main/res/drawable/bg_prefs_color.xml new file mode 100644 index 0000000..beb057e --- /dev/null +++ b/app/src/main/res/drawable/bg_prefs_color.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_search.xml b/app/src/main/res/drawable/bg_search.xml index 99a813c..51781a0 100644 --- a/app/src/main/res/drawable/bg_search.xml +++ b/app/src/main/res/drawable/bg_search.xml @@ -1,7 +1,6 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_searchview.xml b/app/src/main/res/drawable/bg_searchview.xml new file mode 100644 index 0000000..beee4af --- /dev/null +++ b/app/src/main/res/drawable/bg_searchview.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_bottom.png b/app/src/main/res/drawable/bg_shadow_bottom.png new file mode 100644 index 0000000..ac0124b Binary files /dev/null and b/app/src/main/res/drawable/bg_shadow_bottom.png differ diff --git a/app/src/main/res/drawable/bg_touch.xml b/app/src/main/res/drawable/bg_touch.xml deleted file mode 100644 index 141c1c1..0000000 --- a/app/src/main/res/drawable/bg_touch.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/common_label_bg.xml b/app/src/main/res/drawable/common_label_bg.xml index e0ebcee..b3426f6 100644 --- a/app/src/main/res/drawable/common_label_bg.xml +++ b/app/src/main/res/drawable/common_label_bg.xml @@ -1,8 +1,8 @@ - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/fastscroll_bubble.xml b/app/src/main/res/drawable/fastscroll_bubble.xml new file mode 100644 index 0000000..f72b616 --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_bubble.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fastscroll_handle.xml b/app/src/main/res/drawable/fastscroll_handle.xml new file mode 100644 index 0000000..8671a01 --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_handle.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fastscroll_track.xml b/app/src/main/res/drawable/fastscroll_track.xml new file mode 100644 index 0000000..8547687 --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_track.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..c7542ba --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrange.xml b/app/src/main/res/drawable/ic_arrange.xml new file mode 100644 index 0000000..ce2d501 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrange.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..2d68f79 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_down.xml b/app/src/main/res/drawable/ic_arrow_drop_down.xml new file mode 100644 index 0000000..df5ff10 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_up.xml b/app/src/main/res/drawable/ic_arrow_drop_up.xml new file mode 100644 index 0000000..ed31816 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_author.xml b/app/src/main/res/drawable/ic_author.xml new file mode 100644 index 0000000..00d53f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_author.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_auto_page.xml b/app/src/main/res/drawable/ic_auto_page.xml new file mode 100644 index 0000000..bec2a48 --- /dev/null +++ b/app/src/main/res/drawable/ic_auto_page.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_auto_page_stop.xml b/app/src/main/res/drawable/ic_auto_page_stop.xml new file mode 100644 index 0000000..6a6c745 --- /dev/null +++ b/app/src/main/res/drawable/ic_auto_page_stop.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_book_last.xml b/app/src/main/res/drawable/ic_book_last.xml new file mode 100644 index 0000000..82b0842 --- /dev/null +++ b/app/src/main/res/drawable/ic_book_last.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml new file mode 100644 index 0000000..551bee2 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_books.xml b/app/src/main/res/drawable/ic_bottom_books.xml new file mode 100644 index 0000000..807aaaf --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_books.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_books_e.png b/app/src/main/res/drawable/ic_bottom_books_e.png new file mode 100644 index 0000000..ced4b46 Binary files /dev/null and b/app/src/main/res/drawable/ic_bottom_books_e.png differ diff --git a/app/src/main/res/drawable/ic_bottom_books_s.png b/app/src/main/res/drawable/ic_bottom_books_s.png new file mode 100644 index 0000000..6b182e6 Binary files /dev/null and b/app/src/main/res/drawable/ic_bottom_books_s.png differ diff --git a/app/src/main/res/drawable/ic_bottom_mail.xml b/app/src/main/res/drawable/ic_bottom_mail.xml new file mode 100644 index 0000000..8b0251b --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_mail.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_mail_e.png b/app/src/main/res/drawable/ic_bottom_mail_e.png new file mode 100644 index 0000000..86fc01b Binary files /dev/null and b/app/src/main/res/drawable/ic_bottom_mail_e.png differ diff --git a/app/src/main/res/drawable/ic_bottom_mail_s.png b/app/src/main/res/drawable/ic_bottom_mail_s.png new file mode 100644 index 0000000..29d1085 Binary files /dev/null and b/app/src/main/res/drawable/ic_bottom_mail_s.png differ diff --git a/app/src/main/res/drawable/ic_bottom_person.xml b/app/src/main/res/drawable/ic_bottom_person.xml new file mode 100644 index 0000000..5005399 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_person.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_person_e.png b/app/src/main/res/drawable/ic_bottom_person_e.png new file mode 100644 index 0000000..f4895c0 Binary files /dev/null and b/app/src/main/res/drawable/ic_bottom_person_e.png differ diff --git a/app/src/main/res/drawable/ic_bottom_person_s.png b/app/src/main/res/drawable/ic_bottom_person_s.png new file mode 100644 index 0000000..003fa00 Binary files /dev/null and b/app/src/main/res/drawable/ic_bottom_person_s.png differ diff --git a/app/src/main/res/drawable/ic_brightness.xml b/app/src/main/res/drawable/ic_brightness.xml new file mode 100644 index 0000000..ac37ba7 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..620bc91 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml new file mode 100644 index 0000000..dfa860c --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_all.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 0000000..cdf136c --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cursor_left.xml b/app/src/main/res/drawable/ic_cursor_left.xml new file mode 100644 index 0000000..1656763 --- /dev/null +++ b/app/src/main/res/drawable/ic_cursor_left.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cursor_right.xml b/app/src/main/res/drawable/ic_cursor_right.xml new file mode 100644 index 0000000..99734ea --- /dev/null +++ b/app/src/main/res/drawable/ic_cursor_right.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_daytime.xml b/app/src/main/res/drawable/ic_daytime.xml new file mode 100644 index 0000000..60921c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_daytime.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_divider.xml b/app/src/main/res/drawable/ic_divider.xml new file mode 100644 index 0000000..21a88f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_divider.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..bf296ec --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_download_line.xml b/app/src/main/res/drawable/ic_download_line.xml new file mode 100644 index 0000000..25dbe36 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_line.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_google.xml b/app/src/main/res/drawable/ic_google.xml new file mode 100644 index 0000000..da0354a --- /dev/null +++ b/app/src/main/res/drawable/ic_google.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..095e0f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_interface_setting.xml b/app/src/main/res/drawable/ic_interface_setting.xml new file mode 100644 index 0000000..139b007 --- /dev/null +++ b/app/src/main/res/drawable/ic_interface_setting.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 0d025f9..07d5da9 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,7 +5,7 @@ android:viewportWidth="108" android:viewportHeight="108"> + + diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000..091114e --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000..7b7f195 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_cloud_24.xml b/app/src/main/res/drawable/ic_outline_cloud_24.xml new file mode 100644 index 0000000..c8b092e --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_cloud_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_24dp.xml b/app/src/main/res/drawable/ic_pause_24dp.xml new file mode 100644 index 0000000..193030b --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_24dp.xml b/app/src/main/res/drawable/ic_play_24dp.xml new file mode 100644 index 0000000..4250d72 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_rank_type.xml b/app/src/main/res/drawable/ic_rank_type.xml new file mode 100644 index 0000000..1b83c4b --- /dev/null +++ b/app/src/main/res/drawable/ic_rank_type.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_read_aloud.xml b/app/src/main/res/drawable/ic_read_aloud.xml new file mode 100644 index 0000000..7978d21 --- /dev/null +++ b/app/src/main/res/drawable/ic_read_aloud.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 0000000..fc7f629 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml new file mode 100644 index 0000000..9e4f8df --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..921e00d --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_search_hint.xml b/app/src/main/res/drawable/ic_search_hint.xml new file mode 100644 index 0000000..42de64c --- /dev/null +++ b/app/src/main/res/drawable/ic_search_hint.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..1d42977 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml new file mode 100644 index 0000000..eae4da1 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_previous.xml b/app/src/main/res/drawable/ic_skip_previous.xml new file mode 100644 index 0000000..abb9944 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_previous.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stop_black_24dp.xml b/app/src/main/res/drawable/ic_stop_black_24dp.xml new file mode 100644 index 0000000..025a8b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_stop_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_time_add_24dp.xml b/app/src/main/res/drawable/ic_time_add_24dp.xml new file mode 100644 index 0000000..66afd8f --- /dev/null +++ b/app/src/main/res/drawable/ic_time_add_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_toc.xml b/app/src/main/res/drawable/ic_toc.xml new file mode 100644 index 0000000..c64161d --- /dev/null +++ b/app/src/main/res/drawable/ic_toc.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tools_feedback.xml b/app/src/main/res/drawable/ic_tools_feedback.xml new file mode 100644 index 0000000..f2545f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_feedback.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tools_good.xml b/app/src/main/res/drawable/ic_tools_good.xml new file mode 100644 index 0000000..bb3c418 --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_good.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tools_history.xml b/app/src/main/res/drawable/ic_tools_history.xml new file mode 100644 index 0000000..bcc5935 --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_history.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tools_order.xml b/app/src/main/res/drawable/ic_tools_order.xml new file mode 100644 index 0000000..aa6d427 --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_order.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tools_setting.xml b/app/src/main/res/drawable/ic_tools_setting.xml new file mode 100644 index 0000000..d9b6a11 --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_setting.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tools_theme.xml b/app/src/main/res/drawable/ic_tools_theme.xml new file mode 100644 index 0000000..da8789b --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_theme.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_tune.xml b/app/src/main/res/drawable/ic_tune.xml new file mode 100644 index 0000000..cdc9858 --- /dev/null +++ b/app/src/main/res/drawable/ic_tune.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100644 index 0000000..2490a57 --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_view_quilt.xml b/app/src/main/res/drawable/ic_view_quilt.xml new file mode 100644 index 0000000..a0fb3d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_quilt.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_vip.xml b/app/src/main/res/drawable/ic_vip.xml new file mode 100644 index 0000000..6c6597e --- /dev/null +++ b/app/src/main/res/drawable/ic_vip.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml new file mode 100644 index 0000000..c710615 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up.xml b/app/src/main/res/drawable/ic_volume_up.xml new file mode 100644 index 0000000..5d604f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/image_cover_default.png b/app/src/main/res/drawable/image_cover_default.png new file mode 100644 index 0000000..1cc61af Binary files /dev/null and b/app/src/main/res/drawable/image_cover_default.png differ diff --git a/app/src/main/res/drawable/image_loader_loading.xml b/app/src/main/res/drawable/image_loader_loading.xml new file mode 100644 index 0000000..080c38e --- /dev/null +++ b/app/src/main/res/drawable/image_loader_loading.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/line.xml b/app/src/main/res/drawable/line.xml deleted file mode 100644 index e3f2eaa..0000000 --- a/app/src/main/res/drawable/line.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/line_drawable.xml b/app/src/main/res/drawable/line_drawable.xml deleted file mode 100644 index 4f79f47..0000000 --- a/app/src/main/res/drawable/line_drawable.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/loading_anim.xml b/app/src/main/res/drawable/loading_anim.xml new file mode 100644 index 0000000..eedecda --- /dev/null +++ b/app/src/main/res/drawable/loading_anim.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/seekbar_bg.xml b/app/src/main/res/drawable/seekbar_bg.xml deleted file mode 100644 index 804b450..0000000 --- a/app/src/main/res/drawable/seekbar_bg.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/seekbar_thumb.xml b/app/src/main/res/drawable/seekbar_thumb.xml deleted file mode 100644 index 4a2ce8d..0000000 --- a/app/src/main/res/drawable/seekbar_thumb.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/select_book.xml b/app/src/main/res/drawable/select_book.xml deleted file mode 100644 index 1eab0e0..0000000 --- a/app/src/main/res/drawable/select_book.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/select_book_check.xml b/app/src/main/res/drawable/select_book_check.xml deleted file mode 100644 index 35b0743..0000000 --- a/app/src/main/res/drawable/select_book_check.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/select_more.xml b/app/src/main/res/drawable/select_more.xml deleted file mode 100644 index 980222b..0000000 --- a/app/src/main/res/drawable/select_more.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/select_recommend.xml b/app/src/main/res/drawable/select_recommend.xml deleted file mode 100644 index e34d09b..0000000 --- a/app/src/main/res/drawable/select_recommend.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/select_stack.xml b/app/src/main/res/drawable/select_stack.xml deleted file mode 100644 index 572e318..0000000 --- a/app/src/main/res/drawable/select_stack.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_btn_accent_bg.xml b/app/src/main/res/drawable/selector_btn_accent_bg.xml new file mode 100644 index 0000000..d06cfe1 --- /dev/null +++ b/app/src/main/res/drawable/selector_btn_accent_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_category_load.xml b/app/src/main/res/drawable/selector_category_load.xml deleted file mode 100644 index c021f4b..0000000 --- a/app/src/main/res/drawable/selector_category_load.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_category_unload.xml b/app/src/main/res/drawable/selector_category_unload.xml deleted file mode 100644 index d4cc21a..0000000 --- a/app/src/main/res/drawable/selector_category_unload.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_common_bg.xml b/app/src/main/res/drawable/selector_common_bg.xml new file mode 100644 index 0000000..2174a73 --- /dev/null +++ b/app/src/main/res/drawable/selector_common_bg.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_fillet_btn_bg.xml b/app/src/main/res/drawable/selector_fillet_btn_bg.xml new file mode 100644 index 0000000..431a579 --- /dev/null +++ b/app/src/main/res/drawable/selector_fillet_btn_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_tab_color.xml b/app/src/main/res/drawable/selector_tab_color.xml deleted file mode 100644 index bdc5683..0000000 --- a/app/src/main/res/drawable/selector_tab_color.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_card_view.xml b/app/src/main/res/drawable/shape_card_view.xml new file mode 100644 index 0000000..d49a2d5 --- /dev/null +++ b/app/src/main/res/drawable/shape_card_view.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fillet_btn.xml b/app/src/main/res/drawable/shape_fillet_btn.xml new file mode 100644 index 0000000..83ccf27 --- /dev/null +++ b/app/src/main/res/drawable/shape_fillet_btn.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fillet_btn_press.xml b/app/src/main/res/drawable/shape_fillet_btn_press.xml new file mode 100644 index 0000000..b0c46f6 --- /dev/null +++ b/app/src/main/res/drawable/shape_fillet_btn_press.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_text_cursor.xml b/app/src/main/res/drawable/shape_text_cursor.xml new file mode 100644 index 0000000..71871e0 --- /dev/null +++ b/app/src/main/res/drawable/shape_text_cursor.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/text_color.xml b/app/src/main/res/drawable/text_color.xml deleted file mode 100644 index 38b4f6e..0000000 --- a/app/src/main/res/drawable/text_color.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/text_font_color.xml b/app/src/main/res/drawable/text_font_color.xml deleted file mode 100644 index b370650..0000000 --- a/app/src/main/res/drawable/text_font_color.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb.xml b/app/src/main/res/drawable/thumb.xml deleted file mode 100644 index a94705b..0000000 --- a/app/src/main/res/drawable/thumb.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb_drawable.xml b/app/src/main/res/drawable/thumb_drawable.xml deleted file mode 100644 index ebc1534..0000000 --- a/app/src/main/res/drawable/thumb_drawable.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..2fb3504 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_arrange_book.xml b/app/src/main/res/layout/activity_arrange_book.xml new file mode 100644 index 0000000..38f6a84 --- /dev/null +++ b/app/src/main/res/layout/activity_arrange_book.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_detail.xml b/app/src/main/res/layout/activity_book_detail.xml deleted file mode 100644 index 71229d3..0000000 --- a/app/src/main/res/layout/activity_book_detail.xml +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_info.xml b/app/src/main/res/layout/activity_book_info.xml new file mode 100644 index 0000000..43d80d0 --- /dev/null +++ b/app/src/main/res/layout/activity_book_info.xml @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_type_list.xml b/app/src/main/res/layout/activity_book_type_list.xml deleted file mode 100644 index fe781bf..0000000 --- a/app/src/main/res/layout/activity_book_type_list.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_channel.xml b/app/src/main/res/layout/activity_channel.xml new file mode 100644 index 0000000..32293ce --- /dev/null +++ b/app/src/main/res/layout/activity_channel.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_channel_info.xml b/app/src/main/res/layout/activity_channel_info.xml new file mode 100644 index 0000000..9e2bd70 --- /dev/null +++ b/app/src/main/res/layout/activity_channel_info.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chapter_list.xml b/app/src/main/res/layout/activity_chapter_list.xml new file mode 100644 index 0000000..2ee2f3d --- /dev/null +++ b/app/src/main/res/layout/activity_chapter_list.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_daily.xml b/app/src/main/res/layout/activity_daily.xml new file mode 100644 index 0000000..462a4cc --- /dev/null +++ b/app/src/main/res/layout/activity_daily.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_end.xml b/app/src/main/res/layout/activity_end.xml new file mode 100644 index 0000000..2237bd2 --- /dev/null +++ b/app/src/main/res/layout/activity_end.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_feed_back.xml b/app/src/main/res/layout/activity_feed_back.xml new file mode 100644 index 0000000..dab8dba --- /dev/null +++ b/app/src/main/res/layout/activity_feed_back.xml @@ -0,0 +1,50 @@ + + + + + + + + + +